UNPKG

@sveltejs/kit

Version:

Here be dragons, etc etc.

551 lines (456 loc) 13.1 kB
'use strict'; var path = require('path'); var fs = require('fs'); var utils = require('./utils.js'); function posixify(str) { return str.replace(/\\/g, '/'); } const previous_contents = new Map(); function write_if_changed(file, code) { if (code !== previous_contents.get(file)) { previous_contents.set(file, code); utils.mkdirp(path.dirname(file)); fs.writeFileSync(file, code); fudge_mtime(file); } } function fudge_mtime(file) { // need to fudge the mtime so that webpack doesn't go doolally const { atime, mtime } = fs.statSync(file); fs.utimesSync(file, new Date(atime.getTime() - 999999), new Date(mtime.getTime() - 999999)); } const reserved_words = new Set([ 'arguments', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'implements', 'import', 'in', 'instanceof', 'interface', 'let', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'static', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield' ]); function create_manifest_data( cwd, extensions = '.svelte .html' ) { const component_extensions = extensions.split(' '); function find_layout(file_name, component_name, dir = '') { const ext = component_extensions.find((ext) => fs.existsSync(path.join(cwd, dir, `${file_name}${ext}`)) ); const file = posixify(path.join(dir, `${file_name}${ext}`)); const url = `/_app/routes/${file}`; return ext ? { name: component_name, file, url } : null; } const components = []; const pages = []; const endpoints = []; const default_layout = { name: '$default_layout', url: '/_app/assets/components/layout.svelte' }; const default_error = { name: '$default_error', url: '/_app/assets/components/error.svelte' }; function walk( dir, parent_segments, parent_params, stack ) { const items = fs.readdirSync(dir) .map((basename) => { const resolved = path.join(dir, basename); const file = path.relative(cwd, resolved); const is_dir = fs.statSync(resolved).isDirectory(); const ext = path.extname(basename); if (basename[0] === '$') return null; // $layout, $error if (basename[0] === '_') return null; // private files if (basename[0] === '.' && basename !== '.well-known') return null; if (!is_dir && !/^\.[a-z]+$/i.test(ext)) return null; // filter out tmp files etc const segment = is_dir ? basename : basename.slice(0, -ext.length); if (/\]\[/.test(segment)) { throw new Error(`Invalid route ${file} — parameters must be separated`); } const parts = get_parts(segment); const is_index = is_dir ? false : basename.startsWith('index.'); const is_page = component_extensions.indexOf(ext) !== -1; const route_suffix = basename.slice(basename.indexOf('.'), -ext.length); parts.forEach((part) => { if (part.qualifier && /[()?:]/.test(part.qualifier.slice(1, -1))) { throw new Error(`Invalid route ${file} — cannot use (, ), ? or : in route qualifiers`); } }); return { basename, ext, parts, file: posixify(file), is_dir, is_index, is_page, route_suffix }; }) .filter(Boolean) .sort(comparator); items.forEach((item) => { const segments = parent_segments.slice(); if (item.is_index) { if (item.route_suffix) { if (segments.length > 0) { const last_segment = segments[segments.length - 1].slice(); const last_part = last_segment[last_segment.length - 1]; if (last_part.dynamic) { last_segment.push({ dynamic: false, content: item.route_suffix }); } else { last_segment[last_segment.length - 1] = { dynamic: false, content: `${last_part.content}${item.route_suffix}` }; } segments[segments.length - 1] = last_segment; } else { segments.push(item.parts); } } } else { segments.push(item.parts); } const params = parent_params.slice(); params.push(...item.parts.filter((p) => p.dynamic).map((p) => p.content)); if (item.is_dir) { const component = find_layout('$layout', `${get_slug(item.file)}__layout`, item.file); if (component) components.push(component); walk( path.join(dir, item.basename), segments, params, component ? stack.concat({ component, params }) : stack ); } else if (item.is_page) { const component = { name: get_slug(item.file), file: item.file, url: `/_app/routes/${item.file}` }; components.push(component); const parts = item.is_index && stack[stack.length - 1] === null ? stack.slice(0, -1).concat({ component, params }) : stack.concat({ component, params }); const is_static = segments.every((segment) => segment.length === 1 && !segment[0].dynamic); const path = is_static ? `/${segments.map((segment) => segment[0].content).join('/')}` : null; pages.push({ path, pattern: get_pattern(segments, true), parts }); } else { endpoints.push({ name: `route_${get_slug(item.file)}`, pattern: get_pattern(segments, !item.route_suffix), file: item.file, url: `/_app/routes/${item.file}`, params }); } }); } const layout = find_layout('$layout', 'main') || default_layout; const error = find_layout('$error', 'error') || default_error; walk(cwd, [], [], []); // check for clashes const seen_pages = new Map(); pages.forEach((page) => { const pattern = page.pattern.toString(); if (seen_pages.has(pattern)) { const file = page.parts.pop().component.file; const other_page = seen_pages.get(pattern); const other_file = other_page.parts.pop().component.file; throw new Error(`The ${other_file} and ${file} pages clash`); } seen_pages.set(pattern, page); }); const seen_routes = new Map(); endpoints.forEach((route) => { const pattern = route.pattern.toString(); if (seen_routes.has(pattern)) { const other_route = seen_routes.get(pattern); throw new Error(`The ${other_route.file} and ${route.file} routes clash`); } seen_routes.set(pattern, route); }); return { layout, error, components, pages, endpoints }; } function is_spread(path) { const spread_pattern = /\[\.{3}/g; return spread_pattern.test(path); } function comparator(a, b) { if (a.is_index !== b.is_index) { if (a.is_index) return is_spread(a.file) ? 1 : -1; return is_spread(b.file) ? -1 : 1; } const max = Math.max(a.parts.length, b.parts.length); for (let i = 0; i < max; i += 1) { const a_sub_part = a.parts[i]; const b_sub_part = b.parts[i]; if (!a_sub_part) return 1; // b is more specific, so goes first if (!b_sub_part) return -1; // if spread && index, order later if (a_sub_part.spread && b_sub_part.spread) { return a.is_index ? 1 : -1; } // If one is ...spread order it later if (a_sub_part.spread !== b_sub_part.spread) return a_sub_part.spread ? 1 : -1; if (a_sub_part.dynamic !== b_sub_part.dynamic) { return a_sub_part.dynamic ? 1 : -1; } if (!a_sub_part.dynamic && a_sub_part.content !== b_sub_part.content) { return ( b_sub_part.content.length - a_sub_part.content.length || (a_sub_part.content < b_sub_part.content ? -1 : 1) ); } // If both parts dynamic, check for regexp patterns if (a_sub_part.dynamic && b_sub_part.dynamic) { const regexp_pattern = /\((.*?)\)/; const a_match = regexp_pattern.exec(a_sub_part.content); const b_match = regexp_pattern.exec(b_sub_part.content); if (!a_match && b_match) { return 1; // No regexp, so less specific than b } if (!b_match && a_match) { return -1; } if (a_match && b_match && a_match[1] !== b_match[1]) { return b_match[1].length - a_match[1].length; } } } } function get_parts(part) { return part .split(/\[(.+?\(.+?\)|.+?)\]/) .map((str, i) => { if (!str) return null; const dynamic = i % 2 === 1; const [, content, qualifier] = dynamic ? /([^(]+)(\(.+\))?$/.exec(str) : [null, str, null]; return { content, dynamic, spread: dynamic && /^\.{3}.+$/.test(content), qualifier }; }) .filter(Boolean); } function get_slug(file) { let name = file .replace(/[\\/]index/, '') .replace(/[/\\]/g, '_') .replace(/\.\w+$/, '') .replace(/\[([^(]+)(?:\([^(]+\))?\]/, '$$$1') .replace(/[^a-zA-Z0-9_$]/g, (c) => { return c === '.' ? '_' : `$${c.charCodeAt(0)}`; }); if (reserved_words.has(name)) name += '_'; return name; } function get_pattern(segments, add_trailing_slash) { const path = segments .map((segment) => { return segment .map((part) => { return part.dynamic ? part.qualifier || (part.spread ? '(.+)' : '([^/]+?)') : encodeURI(part.content.normalize()) .replace(/\?/g, '%3F') .replace(/#/g, '%23') .replace(/%5B/g, '[') .replace(/%5D/g, ']') .replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }) .join(''); }) .join('\\/'); const trailing = add_trailing_slash && segments.length ? '\\/?$' : '$'; return new RegExp(`^\\/${path}${trailing}`); } function create_app({ manifest_data, output }) { if (!fs.existsSync(output)) utils.mkdirp(output); const client_manifest = generate_client_manifest(manifest_data); const app = generate_app(manifest_data); write_if_changed(`${output}/generated/manifest.js`, client_manifest); write_if_changed(`${output}/generated/root.svelte`, app); } function generate_client_manifest(manifest_data) { const page_ids = new Set(manifest_data.pages.map(page => page.pattern.toString())); const endpoints_to_ignore = manifest_data.endpoints.filter(route => !page_ids.has(route.pattern.toString())); const component_indexes = {}; const components = `[ ${manifest_data.components .map((component, i) => { component_indexes[component.name] = i; return `() => import(${JSON.stringify(component.url)})`; }) .join(',\n\t\t\t\t')} ]`.replace(/^\t/gm, ''); let needs_decode = false; let pages = `[ ${manifest_data.pages .map( (page) => `{ // ${page.parts[page.parts.length - 1].component.file} pattern: ${page.pattern}, parts: [ ${page.parts .map((part) => { const missing_layout = !part; if (missing_layout) return null; if (part.params.length > 0) { needs_decode = true; const props = part.params.map((param, i) => { return param.startsWith('...') ? `${param.slice(3)}: d(m[${i + 1}]).split('/')` : `${param}: d(m[${i + 1}])`; }); return `[components[${component_indexes[part.component.name]}], m => ({ ${props.join(', ')} })]`; } return `[components[${component_indexes[part.component.name]}]]`; }) .filter(Boolean) .join(',\n\t\t\t\t')} ] }`).join(',\n\n\t\t')} ]`.replace(/^\t/gm, ''); if (needs_decode) { pages = `(d => ${pages})(decodeURIComponent)`; } return ` import * as layout from ${JSON.stringify(manifest_data.layout.url)}; const components = ${components}; export const pages = ${pages}; export const ignore = [ ${endpoints_to_ignore.map(route => route.pattern).join(',\n\t\t\t')} ]; export { layout }; ` .replace(/^\t{2}/gm, '') .trim(); } function generate_app(manifest_data) { // TODO remove default layout altogether const max_depth = Math.max( ...manifest_data.pages.map((page) => page.parts.filter(Boolean).length) ); const levels = []; for (let i = 0; i <= max_depth; i += 1) { levels.push(i); } let l = max_depth; let pyramid = `<svelte:component this={components[${l}]} {...(props_${l} || {})}/>`; while (l-- > 1) { pyramid = ` <svelte:component this={components[${l}]} {...(props_${l} || {})}> {#if components[${l + 1}]} ${pyramid.replace(/\n/g, '\n\t\t\t\t\t')} {/if} </svelte:component> ` .replace(/^\t\t\t/gm, '') .trim(); } return ` <!-- This file is generated by @sveltejs/kit — do not edit it! --> <script> import { setContext, afterUpdate } from 'svelte'; import ErrorComponent from ${JSON.stringify(manifest_data.error.url)}; // error handling export let status = undefined; export let error = undefined; // stores export let stores; export let page; export let components; ${levels.map((l) => `export let props_${l} = null;`).join('\n\t\t\t')} const Layout = components[0]; setContext('__svelte__', stores); $: stores.page.set(page); afterUpdate(stores.page.notify); </script> <Layout {...(props_0 || {})}> {#if error} <ErrorComponent {status} {error}/> {:else} ${pyramid.replace(/\n/g, '\n\t\t\t\t')} {/if} </Layout> ` .replace(/^\t\t/gm, '') .trim(); } exports.create_app = create_app; exports.create_manifest_data = create_manifest_data; //# sourceMappingURL=create_app.js.map