229 lines
12 KiB
JavaScript
229 lines
12 KiB
JavaScript
// This takes advantage of `Promise.withResolvers` which is polyfilled in
|
|
// this imported module.
|
|
"use strict";
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
Object.defineProperty(exports, "DefaultRouteMatcherManager", {
|
|
enumerable: true,
|
|
get: function() {
|
|
return DefaultRouteMatcherManager;
|
|
}
|
|
});
|
|
require("../../../lib/polyfill-promise-with-resolvers");
|
|
const _utils = require("../../../shared/lib/router/utils");
|
|
const _localeroutematcher = require("../route-matchers/locale-route-matcher");
|
|
const _ensureleadingslash = require("../../../shared/lib/page-path/ensure-leading-slash");
|
|
class DefaultRouteMatcherManager {
|
|
/**
|
|
* When this value changes, it indicates that a change has been introduced
|
|
* that requires recompilation.
|
|
*/ get compilationID() {
|
|
return this.providers.length;
|
|
}
|
|
async waitTillReady() {
|
|
if (this.waitTillReadyPromise) {
|
|
await this.waitTillReadyPromise;
|
|
delete this.waitTillReadyPromise;
|
|
}
|
|
}
|
|
async reload() {
|
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
this.waitTillReadyPromise = promise;
|
|
// Grab the compilation ID for this run, we'll verify it at the end to
|
|
// ensure that if any routes were added before reloading is finished that
|
|
// we error out.
|
|
const compilationID = this.compilationID;
|
|
try {
|
|
// Collect all the matchers from each provider.
|
|
const matchers = [];
|
|
// Get all the providers matchers.
|
|
const providersMatchers = await Promise.all(this.providers.map((provider)=>provider.matchers()));
|
|
// Use this to detect duplicate pathnames.
|
|
const all = new Map();
|
|
const duplicates = {};
|
|
for (const providerMatchers of providersMatchers){
|
|
for (const matcher of providerMatchers){
|
|
// Reset duplicated matches when reloading from pages conflicting state.
|
|
if (matcher.duplicated) delete matcher.duplicated;
|
|
// Test to see if the matcher being added is a duplicate.
|
|
const duplicate = all.get(matcher.definition.pathname);
|
|
if (duplicate) {
|
|
// This looks a little weird, but essentially if the pathname
|
|
// already exists in the duplicates map, then we got that array
|
|
// reference. Otherwise, we create a new array with the original
|
|
// duplicate first. Then we push the new matcher into the duplicate
|
|
// array, and reset it to the duplicates object (which may be a
|
|
// no-op if the pathname already existed in the duplicates object).
|
|
// Then we set the array of duplicates on both the original
|
|
// duplicate object and the new one, so we can keep them in sync.
|
|
// If a new duplicate is found, and it matches an existing pathname,
|
|
// the retrieval of the `other` will actually return the array
|
|
// reference used by all other duplicates. This is why ReadonlyArray
|
|
// is so important! Array's are always references!
|
|
const others = duplicates[matcher.definition.pathname] ?? [
|
|
duplicate
|
|
];
|
|
others.push(matcher);
|
|
duplicates[matcher.definition.pathname] = others;
|
|
// Add duplicated details to each route.
|
|
duplicate.duplicated = others;
|
|
matcher.duplicated = others;
|
|
// TODO: see if we should error for duplicates in production?
|
|
}
|
|
matchers.push(matcher);
|
|
// Add the matcher's pathname to the set.
|
|
all.set(matcher.definition.pathname, matcher);
|
|
}
|
|
}
|
|
// Update the duplicate matchers. This is used in the development manager
|
|
// to warn about duplicates.
|
|
this.matchers.duplicates = duplicates;
|
|
// If the cache is the same as what we just parsed, we can exit now. We
|
|
// can tell by using the `===` which compares object identity, which for
|
|
// the manifest matchers, will return the same matcher each time.
|
|
if (this.previousMatchers.length === matchers.length && this.previousMatchers.every((cachedMatcher, index)=>cachedMatcher === matchers[index])) {
|
|
return;
|
|
}
|
|
this.previousMatchers = matchers;
|
|
// For matchers that are for static routes, filter them now.
|
|
this.matchers.static = matchers.filter((matcher)=>!matcher.isDynamic);
|
|
// For matchers that are for dynamic routes, filter them and sort them now.
|
|
const dynamic = matchers.filter((matcher)=>matcher.isDynamic);
|
|
// As `getSortedRoutes` only takes an array of strings, we need to create
|
|
// a map of the pathnames (used for sorting) and the matchers. When we
|
|
// have locales, there may be multiple matches for the same pathname. To
|
|
// handle this, we keep a map of all the indexes (in `reference`) and
|
|
// merge them in later.
|
|
const reference = new Map();
|
|
const pathnames = new Array();
|
|
for(let index = 0; index < dynamic.length; index++){
|
|
// Grab the pathname from the definition.
|
|
const pathname = dynamic[index].definition.pathname;
|
|
// Grab the index in the dynamic array, push it into the reference.
|
|
const indexes = reference.get(pathname) ?? [];
|
|
indexes.push(index);
|
|
// If this is the first one set it. If it isn't, we don't need to
|
|
// because pushing above on the array will mutate the array already
|
|
// stored there because array's are always a reference!
|
|
if (indexes.length === 1) reference.set(pathname, indexes);
|
|
else continue;
|
|
pathnames.push(pathname);
|
|
}
|
|
// Sort the array of pathnames.
|
|
const sorted = (0, _utils.getSortedRoutes)(pathnames);
|
|
// For each of the sorted pathnames, iterate over them, grabbing the list
|
|
// of indexes and merging them back into the new `sortedDynamicMatchers`
|
|
// array. The order of the same matching pathname doesn't matter because
|
|
// they will have other matching characteristics (like the locale) that
|
|
// is considered.
|
|
const sortedDynamicMatchers = [];
|
|
for (const pathname of sorted){
|
|
const indexes = reference.get(pathname);
|
|
if (!Array.isArray(indexes)) {
|
|
throw new Error("Invariant: expected to find identity in indexes map");
|
|
}
|
|
const dynamicMatches = indexes.map((index)=>dynamic[index]);
|
|
sortedDynamicMatchers.push(...dynamicMatches);
|
|
}
|
|
this.matchers.dynamic = sortedDynamicMatchers;
|
|
// This means that there was a new matcher pushed while we were waiting
|
|
if (this.compilationID !== compilationID) {
|
|
throw new Error("Invariant: expected compilation to finish before new matchers were added, possible missing await");
|
|
}
|
|
} catch (err) {
|
|
reject(err);
|
|
} finally{
|
|
// The compilation ID matched, so mark the complication as finished.
|
|
this.lastCompilationID = compilationID;
|
|
resolve();
|
|
}
|
|
}
|
|
push(provider) {
|
|
this.providers.push(provider);
|
|
}
|
|
async test(pathname, options) {
|
|
// See if there's a match for the pathname...
|
|
const match = await this.match(pathname, options);
|
|
// This default implementation only needs to check to see if there _was_ a
|
|
// match. The development matcher actually changes it's behavior by not
|
|
// recompiling the routes.
|
|
return match !== null;
|
|
}
|
|
async match(pathname, options) {
|
|
// "Iterate" over the match options. Once we found a single match, exit with
|
|
// it, otherwise return null below. If no match is found, the inner block
|
|
// won't be called.
|
|
for await (const match of this.matchAll(pathname, options)){
|
|
return match;
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* This is a point for other managers to override to inject other checking
|
|
* behavior like duplicate route checking on a per-request basis.
|
|
*
|
|
* @param pathname the pathname to validate against
|
|
* @param matcher the matcher to validate/test with
|
|
* @returns the match if found
|
|
*/ validate(pathname, matcher, options) {
|
|
var _options_i18n;
|
|
if (matcher instanceof _localeroutematcher.LocaleRouteMatcher) {
|
|
return matcher.match(pathname, options);
|
|
}
|
|
// If the locale was inferred from the default locale, then it will have
|
|
// already added a locale to the pathname. We need to remove it before
|
|
// matching because this matcher is not locale aware.
|
|
if ((_options_i18n = options.i18n) == null ? void 0 : _options_i18n.inferredFromDefault) {
|
|
return matcher.match(options.i18n.pathname);
|
|
}
|
|
return matcher.match(pathname);
|
|
}
|
|
async *matchAll(pathname, options) {
|
|
// Guard against the matcher manager from being run before it needs to be
|
|
// recompiled. This was preferred to re-running the compilation here because
|
|
// it should be re-ran only when it changes. If a match is attempted before
|
|
// this is done, it indicates that there is a case where a provider is added
|
|
// before it was recompiled (an error). We also don't want to affect request
|
|
// times.
|
|
if (this.lastCompilationID !== this.compilationID) {
|
|
throw new Error("Invariant: expected routes to have been loaded before match");
|
|
}
|
|
// Ensure that path matching is done with a leading slash.
|
|
pathname = (0, _ensureleadingslash.ensureLeadingSlash)(pathname);
|
|
// If this pathname doesn't look like a dynamic route, and this pathname is
|
|
// listed in the normalized list of routes, then return it. This ensures
|
|
// that when a route like `/user/[id]` is encountered, it doesn't just match
|
|
// with the list of normalized routes.
|
|
if (!(0, _utils.isDynamicRoute)(pathname)) {
|
|
for (const matcher of this.matchers.static){
|
|
const match = this.validate(pathname, matcher, options);
|
|
if (!match) continue;
|
|
yield match;
|
|
}
|
|
}
|
|
// If we should skip handling dynamic routes, exit now.
|
|
if (options == null ? void 0 : options.skipDynamic) return null;
|
|
// Loop over the dynamic matchers, yielding each match.
|
|
for (const matcher of this.matchers.dynamic){
|
|
const match = this.validate(pathname, matcher, options);
|
|
if (!match) continue;
|
|
yield match;
|
|
}
|
|
// We tried direct matching against the pathname and against all the dynamic
|
|
// paths, so there was no match.
|
|
return null;
|
|
}
|
|
constructor(){
|
|
this.providers = [];
|
|
this.matchers = {
|
|
static: [],
|
|
dynamic: [],
|
|
duplicates: {}
|
|
};
|
|
this.lastCompilationID = this.compilationID;
|
|
this.previousMatchers = [];
|
|
}
|
|
}
|
|
|
|
//# sourceMappingURL=default-route-matcher-manager.js.map
|