@sveltejs/kit
Version:
Here be dragons, etc etc.
551 lines (456 loc) • 13.1 kB
JavaScript
;
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