diff --git a/runtime/src/app/app.ts b/runtime/src/app/app.ts index 95b882fd0..b38ab72a5 100644 --- a/runtime/src/app/app.ts +++ b/runtime/src/app/app.ts @@ -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]; @@ -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 }; } } } diff --git a/runtime/src/app/goto/index.ts b/runtime/src/app/goto/index.ts index 0241b2c65..695fc0f5b 100644 --- a/runtime/src/app/goto/index.ts +++ b/runtime/src/app/goto/index.ts @@ -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(() => {}); } diff --git a/runtime/src/app/start/index.ts b/runtime/src/app/start/index.ts index 858eb32ea..a36ff4a80 100644 --- a/runtime/src/app/start/index.ts +++ b/runtime/src/app/start/index.ts @@ -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); }); } @@ -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); } } diff --git a/runtime/src/app/types.ts b/runtime/src/app/types.ts index 51a787dc3..fed219dcd 100644 --- a/runtime/src/app/types.ts +++ b/runtime/src/app/types.ts @@ -48,6 +48,7 @@ export type Target = { route: Route; match: RegExpExecArray; page: Page; + pathname: string; }; export type Redirect = { diff --git a/runtime/src/server/middleware/get_page_handler.ts b/runtime/src/server/middleware/get_page_handler.ts index 53f413640..f9e24d913 100644 --- a/runtime/src/server/middleware/get_page_handler.ts +++ b/runtime/src/server/middleware/get_page_handler.ts @@ -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', @@ -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); @@ -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(', '); @@ -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 += ` + `; + } + let styles: string; // TODO make this consistent across apps @@ -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%', () => ``) - .replace('%sapper.scripts%', () => `${script}`) + .replace('%sapper.base%', () => ``) + .replace('%sapper.scripts%', () => `${pre_script}${script}`) .replace('%sapper.html%', () => html) .replace('%sapper.head%', () => `${head}`) .replace('%sapper.styles%', () => styles); diff --git a/runtime/src/server/middleware/index.ts b/runtime/src/server/middleware/index.ts index f7bbea4ff..5b2acd415 100644 --- a/runtime/src/server/middleware/index.ts +++ b/runtime/src/server/middleware/index.ts @@ -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] !== '/') { diff --git a/src/api/export.ts b/src/api/export.ts index 008224910..9fdf193f2 100644 --- a/src/api/export.ts +++ b/src/api/export.ts @@ -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 = { @@ -55,7 +56,8 @@ async function _export({ concurrent = 8, oninfo = noop, onfile = noop, - entry = '/' + entry = '/', + relative_basepath = false, }: Opts = {}) { basepath = basepath.replace(/^\//, '') @@ -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) }); diff --git a/src/cli.ts b/src/cli.ts index 97c95df76..bac20af52 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -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, @@ -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) { @@ -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}`)); },