Skip to content
This repository has been archived by the owner on Jan 11, 2023. It is now read-only.

Support for relative base path to allow website to be served from different paths #866

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 61 additions & 8 deletions runtime/src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,71 @@ export function extract_query(search: string) {
return query;
}

export function select_target(url: URL): Target {
if (url.origin !== location.origin) return null;
if (!url.pathname.startsWith(initial_data.baseUrl)) return null;
function countBaseDepth(baseUrl: string): number {
let depth = 0;
const splitted = baseUrl.split('/');
for (let split of splitted) {
if (split === '..') {
depth++;
}
}
return depth;
}

function trimSlashes(path: string): string {
if (path.startsWith('/')) {
path = path.slice(1);
}
if (path.endsWith('/')) {
path = path.slice(0, path.length - 1);
}
return path;
}

let path = url.pathname.slice(initial_data.baseUrl.length);
let pathToCut;
export function select_target(url: URL, start: boolean = false): Target {
if (url.origin !== location.origin) return null;

if (path === '') {
path = '/';
let path = url.pathname;
let pathname = url.pathname;
let normalPath = pathname;
if (initial_data.baseUrl.startsWith('.')) {
if(path.endsWith('index.html')) {
path = path.slice(0, path.length-10);
}
if (start) {
const baseDepth = countBaseDepth(initial_data.baseUrl);
const splitPath = trimSlashes(path).split('/');
pathToCut = '/' + splitPath.slice(0, splitPath.length-baseDepth).join('/');
}
path = path.substr(pathToCut.length);
if (!path.startsWith('/')) {
path = '/' + path;
}
normalPath = path;
if(!path.endsWith('/')) {
path = path + '/';
}
if(pathToCut === '/') {
pathname = path;
} else {
pathname = pathToCut + path;
}
if (path === '') {
path = '/';
pathname = pathToCut;
}
} else {
if (!path.startsWith(initial_data.baseUrl)) return null;
path = path.slice(initial_data.baseUrl.length);
if (path === '') {
path = '/';
}
normalPath = path;
}

// avoid accidental clashes between server routes and page routes
if (ignore.some(pattern => pattern.test(path))) return;
if (ignore.some(pattern => pattern.test(normalPath))) return;

for (let i = 0; i < routes.length; i += 1) {
const route = routes[i];
Expand All @@ -118,7 +171,7 @@ export function select_target(url: URL): Target {

const page = { host: location.host, path, query, params };

return { href: url.href, route, match, page };
return { href: url.href, pathname, route, match, page };
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/app/goto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function goto(href: string, opts = { replaceState: false }) {
const target = select_target(new URL(href, document.baseURI));

if (target) {
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', href);
history[opts.replaceState ? 'replaceState' : 'pushState']({ id: cid }, '', target.pathname);
return navigate(target, null).then(() => {});
}

Expand Down
4 changes: 2 additions & 2 deletions runtime/src/app/start/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function start(opts: {

if (initial_data.error) return handle_error(url);

const target = select_target(url);
const target = select_target(url, true);
if (target) return navigate(target, uid, true, hash);
});
}
Expand Down Expand Up @@ -100,7 +100,7 @@ function handle_click(event: MouseEvent) {
const noscroll = a.hasAttribute('sapper-noscroll');
navigate(target, null, noscroll, url.hash);
event.preventDefault();
history.pushState({ id: cid }, '', url.href);
history.pushState({ id: cid }, '', target.pathname);
}
}

Expand Down
1 change: 1 addition & 0 deletions runtime/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type Target = {
route: Route;
match: RegExpExecArray;
page: Page;
pathname: string;
};

export type Redirect = {
Expand Down
74 changes: 64 additions & 10 deletions runtime/src/server/middleware/get_page_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export function get_page_handler(
}

async function handle_page(page: Page, req: Req, res: Res, status = 200, error: Error | string = null) {
let baseHref = req.baseUrl;
if(req.params && req.params.baseHref) {
baseHref = req.params.baseHref;
}
const prefix = baseHref.startsWith('.') ? '' : baseHref + '/';
const is_service_worker_index = req.path === '/service-worker-index.html';
const build_info: {
bundler: 'rollup' | 'webpack',
Expand Down Expand Up @@ -72,7 +77,7 @@ export function get_page_handler(
// TODO add dependencies and CSS
const link = preloaded_chunks
.filter(file => file && !file.match(/\.map$/))
.map(file => `<${req.baseUrl}/client/${file}>;rel="modulepreload"`)
.map(file => `<${prefix}client/${file}>;rel="modulepreload"`)
.join(', ');

res.setHeader('Link', link);
Expand All @@ -81,7 +86,7 @@ export function get_page_handler(
.filter(file => file && !file.match(/\.map$/))
.map((file) => {
const as = /\.css$/.test(file) ? 'style' : 'script';
return `<${req.baseUrl}/client/${file}>;rel="preload";as="${as}"`;
return `<${prefix}client/${file}>;rel="preload";as="${as}"`;
})
.join(', ');

Expand Down Expand Up @@ -269,29 +274,78 @@ export function get_page_handler(

let script = `__SAPPER__={${[
error && `error:${serialized.error},status:${status}`,
`baseUrl:"${req.baseUrl}"`,
`baseUrl:"${baseHref}"`,
serialized.preloaded && `preloaded:${serialized.preloaded}`,
serialized.session && `session:${serialized.session}`
].filter(Boolean).join(',')}};`;

if (has_service_worker) {
script += `if('serviceWorker' in navigator)navigator.serviceWorker.register('${req.baseUrl}/service-worker.js');`;
script += `if('serviceWorker' in navigator)navigator.serviceWorker.register('${prefix}service-worker.js');`;
}

const file = [].concat(build_info.assets.main).filter(file => file && /\.js$/.test(file))[0];
const main = `${req.baseUrl}/client/${file}`;
const main = `${prefix}client/${file}`;

if (build_info.bundler === 'rollup') {
if (build_info.legacy_assets) {
const legacy_main = `${req.baseUrl}/client/legacy/${build_info.legacy_assets.main}`;
script += `(function(){try{eval("async function x(){}");var main="${main}"}catch(e){main="${legacy_main}"};var s=document.createElement("script");try{new Function("if(0)import('')")();s.src=main;s.type="module";s.crossOrigin="use-credentials";}catch(e){s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main",main);}document.head.appendChild(s);}());`;
const legacy_main = `${prefix}client/legacy/${build_info.legacy_assets.main}`;
script += `(function(){try{eval("async function x(){}");var main="${main}"}catch(e){main="${legacy_main}"};var s=document.createElement("script");try{new Function("if(0)import('')")();s.src=main;s.type="module";s.crossOrigin="use-credentials";}catch(e){s.src="${prefix}client/shimport@${build_info.shimport}.js";s.setAttribute("data-main",main);}document.head.appendChild(s);}());`;
} else {
script += `var s=document.createElement("script");try{new Function("if(0)import('')")();s.src="${main}";s.type="module";s.crossOrigin="use-credentials";}catch(e){s.src="${req.baseUrl}/client/shimport@${build_info.shimport}.js";s.setAttribute("data-main","${main}")}document.head.appendChild(s)`;
script += `var s=document.createElement("script");try{new Function("if(0)import('')")();s.src="${main}";s.type="module";s.crossOrigin="use-credentials";}catch(e){s.src="${prefix}client/shimport@${build_info.shimport}.js";s.setAttribute("data-main","${main}")}document.head.appendChild(s)`;
}
} else {
script += `</script><script src="${main}">`;
}

let pre_script = '';
// This force redirect to canonical url so links/scripts... can be fetched:
// This simply ensure path finish by "/"
// this will also redirect from /index.html
if(baseHref.startsWith('.')) {
pre_script = `
<script>
function countBaseDepth(baseUrl) {
let depth = 0;
const splitted = baseUrl.split('/');
for (let split of splitted) {
if (split === '..') {
depth++;
}
}
return depth;
}

function trimSlashes(path) {
if (path.startsWith('/')) {
path = path.slice(1);
}
if (path.endsWith('/')) {
path = path.slice(0, path.length - 1);
}
return path;
}
var baseHref = document.getElementsByTagName('base')[0].getAttribute('href');
var baseDepth = countBaseDepth(baseHref);
var splitPath = trimSlashes(location.pathname).split('/');
var pathToCut = '/' + splitPath.slice(0, splitPath.length-baseDepth).join('/');
var hrefBeforeParams = location.href;
var paramsString = '';
var paramsIndex = hrefBeforeParams.indexOf('?');
if (paramsIndex > -1) {
paramsString = hrefBeforeParams.substring(paramsIndex);
hrefBeforeParams = hrefBeforeParams.substring(0, paramsIndex);
}
if (hrefBeforeParams.endsWith('index.html')) {
hrefBeforeParams = hrefBeforeParams.slice(0, hrefBeforeParams.length - 10);
location.replace(hrefBeforeParams + paramsString);
} else if(!hrefBeforeParams.endsWith('/')) {
hrefBeforeParams = hrefBeforeParams + '/';
location.replace(hrefBeforeParams + paramsString);
}
</script>
`;
}

let styles: string;

// TODO make this consistent across apps
Expand Down Expand Up @@ -321,8 +375,8 @@ export function get_page_handler(
const nonce_attr = (res.locals && res.locals.nonce) ? ` nonce="${res.locals.nonce}"` : '';

const body = template()
.replace('%sapper.base%', () => `<base href="${req.baseUrl}/">`)
.replace('%sapper.scripts%', () => `<script${nonce_attr}>${script}</script>`)
.replace('%sapper.base%', () => `<base href="${baseHref}/">`)
.replace('%sapper.scripts%', () => `${pre_script}<script${nonce_attr}>${script}</script>`)
.replace('%sapper.html%', () => html)
.replace('%sapper.head%', () => `<noscript id='sapper-head-start'></noscript>${head}<noscript id='sapper-head-end'></noscript>`)
.replace('%sapper.styles%', () => styles);
Expand Down
18 changes: 18 additions & 0 deletions runtime/src/server/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,26 @@ export default function middleware(opts: {

let emitted_basepath = false;

const useRelativeBasePath = process.env['SAPPER_RELATIVE_BASEPATH'] === 'true';

return compose_handlers(ignore, [
(req: Req, res: Res, next: () => void) => {
if (useRelativeBasePath) {
if (!req.params) {
req.params = {}
}
if(req.url === '/') {
req.params.baseHref = '.';
} else {
const numParts = req.path.split('/').length;
if (numParts > 1) {
req.params.baseHref = '..';
for (let i = 2; i < numParts; i++) {
req.params.baseHref += '/..';
}
}
}
}
if (req.baseUrl === undefined) {
let { originalUrl } = req;
if (req.url === '/' && originalUrl[originalUrl.length - 1] !== '/') {
Expand Down
7 changes: 5 additions & 2 deletions src/api/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Opts = {
oninfo?: ({ message }: { message: string }) => void;
onfile?: ({ file, size, status }: { file: string, size: number, status: number }) => void;
entry?: string;
relative_basepath?: boolean;
};

type Ref = {
Expand Down Expand Up @@ -55,7 +56,8 @@ async function _export({
concurrent = 8,
oninfo = noop,
onfile = noop,
entry = '/'
entry = '/',
relative_basepath = false,
}: Opts = {}) {
basepath = basepath.replace(/^\//, '')

Expand Down Expand Up @@ -94,7 +96,8 @@ async function _export({
env: Object.assign({
PORT: port,
NODE_ENV: 'production',
SAPPER_EXPORT: 'true'
SAPPER_EXPORT: 'true',
SAPPER_RELATIVE_BASEPATH: relative_basepath ? 'true' : undefined,
}, process.env)
});

Expand Down
6 changes: 4 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ prog.command('export [dest]')
.option('--build-dir', 'Intermediate build directory', '__sapper__/build')
.option('--ext', 'Custom page route extensions (space separated)', '.svelte .html')
.option('--entry', 'Custom entry points (space separated)', '/')
.option('--relative-basepath', 'so exported site can be put in any basepath', false)
.action(async (dest = '__sapper__/export', opts: {
build: boolean,
legacy: boolean,
Expand All @@ -222,7 +223,8 @@ prog.command('export [dest]')
output: string,
'build-dir': string,
ext: string
entry: string
entry: string,
'relative-basepath': boolean,
}) => {
try {
if (opts.build) {
Expand All @@ -244,7 +246,7 @@ prog.command('export [dest]')
timeout: opts.timeout,
concurrent: opts.concurrent,
entry: opts.entry,

relative_basepath: opts['relative-basepath'],
oninfo: event => {
console.log(colors.bold().cyan(`> ${event.message}`));
},
Expand Down