463 lines
12 KiB
JavaScript
463 lines
12 KiB
JavaScript
/**
|
|
* Type system for flight data elements
|
|
*/
|
|
|
|
import { join } from '../utils.js';
|
|
|
|
const ENABLE_TYPE_VERIF = true;
|
|
|
|
/**
|
|
* Base class for all flight data elements
|
|
*/
|
|
export class Element {
|
|
constructor(value, value_class, index = null) {
|
|
this.value = value;
|
|
this.value_class = value_class;
|
|
this.index = index;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents a "HL" (HintPreload) object for preload hints
|
|
*/
|
|
export class HintPreload extends Element {
|
|
constructor(value, value_class, index = null) {
|
|
super(value, value_class, index);
|
|
|
|
if (ENABLE_TYPE_VERIF) {
|
|
if (!Array.isArray(this.value)) throw new TypeError('HintPreload value must be array');
|
|
if (this.value.length < 2 || this.value.length > 3) {
|
|
throw new TypeError('HintPreload value must have 2-3 elements');
|
|
}
|
|
if (typeof this.href !== 'string') throw new TypeError('HintPreload href must be string');
|
|
if (typeof this.type_name !== 'string') throw new TypeError('HintPreload type_name must be string');
|
|
if (this.attrs !== null && typeof this.attrs !== 'object') {
|
|
throw new TypeError('HintPreload attrs must be null or object');
|
|
}
|
|
}
|
|
}
|
|
|
|
get href() {
|
|
return this.value[0];
|
|
}
|
|
|
|
get type_name() {
|
|
return this.value[1];
|
|
}
|
|
|
|
get attrs() {
|
|
return this.value.length >= 3 ? this.value[2] : null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents a "I" (Module) object for module imports
|
|
*/
|
|
export class Module extends Element {
|
|
constructor(value, value_class, index = null) {
|
|
super(value, value_class, index);
|
|
|
|
if (ENABLE_TYPE_VERIF) {
|
|
if (!Array.isArray(this.value) && typeof this.value !== 'object') {
|
|
throw new TypeError('Module value must be array or object');
|
|
}
|
|
if (Object.keys(this.value).length < 3 || Object.keys(this.value).length > 4) {
|
|
throw new TypeError('Module value must have 3-4 elements');
|
|
}
|
|
if (typeof this.module_id !== 'number') throw new TypeError('Module module_id must be number');
|
|
if (Array.isArray(this.value)) {
|
|
if (!Array.isArray(this.value[1])) throw new TypeError('Module chunks must be array');
|
|
if (this.value[1].length % 2 !== 0) throw new TypeError('Module chunks must have even length');
|
|
} else {
|
|
if (!Array.isArray(this.value.chunks)) throw new TypeError('Module chunks must be array');
|
|
}
|
|
if (typeof this.module_name !== 'string') throw new TypeError('Module module_name must be string');
|
|
}
|
|
}
|
|
|
|
get module_id() {
|
|
return Array.isArray(this.value) ? this.value[0] : parseInt(this.value.id);
|
|
}
|
|
|
|
module_chunks_raw() {
|
|
if (Array.isArray(this.value)) {
|
|
const result = {};
|
|
for (let i = 0; i < this.value[1].length; i += 2) {
|
|
result[this.value[1][i]] = this.value[1][i + 1];
|
|
}
|
|
return result;
|
|
} else {
|
|
const result = {};
|
|
for (const item of this.value.chunks) {
|
|
const [key, val] = item.split(':', 2);
|
|
result[key] = val;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
get module_chunks() {
|
|
const raw = this.module_chunks_raw();
|
|
const result = {};
|
|
for (const [key, value] of Object.entries(raw)) {
|
|
result[key] = join('/_next', value);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
get module_name() {
|
|
return Array.isArray(this.value) ? this.value[2] : this.value.name;
|
|
}
|
|
|
|
get is_async() {
|
|
return typeof this.value === 'object' && !Array.isArray(this.value) ? this.value.async : false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents a "T" (Text) element
|
|
*/
|
|
export class Text extends Element {
|
|
constructor(value, value_class, index = null) {
|
|
super(value, value_class, index);
|
|
|
|
if (ENABLE_TYPE_VERIF) {
|
|
if (typeof this.value !== 'string') throw new TypeError('Text value must be string');
|
|
}
|
|
}
|
|
|
|
get text() {
|
|
return this.value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents flight data with content
|
|
*/
|
|
export class Data extends Element {
|
|
constructor(value, value_class, index = null) {
|
|
super(value, value_class, index);
|
|
|
|
if (ENABLE_TYPE_VERIF) {
|
|
if (!isFlightDataObj(this.value)) throw new TypeError('Data value must be flight data object');
|
|
if (this.content !== null && typeof this.content !== 'object') {
|
|
throw new TypeError('Data content must be null or object');
|
|
}
|
|
}
|
|
}
|
|
|
|
get content() {
|
|
return this.value[3];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents empty/null data
|
|
*/
|
|
export class EmptyData extends Element {
|
|
constructor(value, value_class, index = null) {
|
|
super(value, value_class, index);
|
|
|
|
if (ENABLE_TYPE_VERIF) {
|
|
if (this.value !== null) throw new TypeError('EmptyData value must be null');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents special data markers (strings starting with "$")
|
|
*/
|
|
export class SpecialData extends Element {
|
|
constructor(value, value_class, index = null) {
|
|
super(value, value_class, index);
|
|
|
|
if (ENABLE_TYPE_VERIF) {
|
|
if (typeof this.value !== 'string') throw new TypeError('SpecialData value must be string');
|
|
if (!this.value.startsWith('$')) throw new TypeError('SpecialData value must start with $');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents HTML elements in flight data
|
|
*/
|
|
export class HTMLElement extends Element {
|
|
constructor(value, value_class, index = null) {
|
|
super(value, value_class, index);
|
|
|
|
if (ENABLE_TYPE_VERIF) {
|
|
if (!isFlightDataObj(this.value)) throw new TypeError('HTMLElement value must be flight data object');
|
|
if (typeof this.tag !== 'string') throw new TypeError('HTMLElement tag must be string');
|
|
if (this.href !== null && typeof this.href !== 'string') {
|
|
throw new TypeError('HTMLElement href must be null or string');
|
|
}
|
|
if (typeof this.attrs !== 'object') throw new TypeError('HTMLElement attrs must be object');
|
|
}
|
|
}
|
|
|
|
get tag() {
|
|
return this.value[1];
|
|
}
|
|
|
|
get href() {
|
|
return this.value[2];
|
|
}
|
|
|
|
get attrs() {
|
|
return this.value[3];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents a container of multiple data elements
|
|
*/
|
|
export class DataContainer extends Element {
|
|
constructor(value, value_class, index = null) {
|
|
super(value, value_class, index);
|
|
|
|
// Resolve all contained elements
|
|
this.value = this.value.map(item => resolveType(item, null, null));
|
|
|
|
if (ENABLE_TYPE_VERIF) {
|
|
if (!this.value.every(item => item instanceof Element)) {
|
|
throw new TypeError('DataContainer value must contain only Elements');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents a parent element with children
|
|
*/
|
|
export class DataParent extends Element {
|
|
constructor(value, value_class, index = null) {
|
|
super(value, value_class, index);
|
|
|
|
// Resolve children element
|
|
this.value[3].children = resolveType(this.value[3].children, null, null);
|
|
|
|
if (ENABLE_TYPE_VERIF) {
|
|
if (!isFlightDataObj(this.value)) throw new TypeError('DataParent value must be flight data object');
|
|
if (!(this.children instanceof Element)) throw new TypeError('DataParent children must be Element');
|
|
}
|
|
}
|
|
|
|
get children() {
|
|
return this.value[3].children;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents URL query parameters
|
|
*/
|
|
export class URLQuery extends Element {
|
|
constructor(value, value_class, index = null) {
|
|
super(value, value_class, index);
|
|
|
|
if (ENABLE_TYPE_VERIF) {
|
|
if (this.value.length !== 3) throw new TypeError('URLQuery value must have 3 elements');
|
|
if (typeof this.key !== 'string') throw new TypeError('URLQuery key must be string');
|
|
if (typeof this.val !== 'string') throw new TypeError('URLQuery val must be string');
|
|
}
|
|
}
|
|
|
|
get key() {
|
|
return this.value[0];
|
|
}
|
|
|
|
get val() {
|
|
return this.value[1];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* RSC Payload version enum
|
|
*/
|
|
export const RSCPayloadVersion = {
|
|
old: 0,
|
|
new: 1
|
|
};
|
|
|
|
/**
|
|
* Represents React Server Components payload
|
|
*/
|
|
export class RSCPayload extends Element {
|
|
constructor(value, value_class, index = null) {
|
|
super(value, value_class, index);
|
|
|
|
if (ENABLE_TYPE_VERIF) {
|
|
if (!isFlightDataObj(this.value) && typeof this.value !== 'object') {
|
|
throw new TypeError('RSCPayload value must be flight data object or object');
|
|
}
|
|
if (typeof this.build_id !== 'string') throw new TypeError('RSCPayload build_id must be string');
|
|
}
|
|
}
|
|
|
|
_version() {
|
|
if (Array.isArray(this.value) && this.value.length === 4) {
|
|
return RSCPayloadVersion.old;
|
|
} else if (typeof this.value === 'object' && 'b' in this.value) {
|
|
return RSCPayloadVersion.new;
|
|
} else {
|
|
throw new Error('Unknown flight RSC payload version');
|
|
}
|
|
}
|
|
|
|
get build_id() {
|
|
const version = this._version();
|
|
if (version === RSCPayloadVersion.new) {
|
|
return this.value.b;
|
|
} else {
|
|
return this.value[3].buildId;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents an error element
|
|
*/
|
|
export class Error extends Element {
|
|
constructor(value, value_class, index = null) {
|
|
super(value, value_class, index);
|
|
|
|
if (ENABLE_TYPE_VERIF) {
|
|
if (typeof this.value !== 'object') throw new TypeError('Error value must be object');
|
|
if (!('digest' in this.value)) throw new TypeError('Error value must have digest');
|
|
if (typeof this.digest !== 'string') throw new TypeError('Error digest must be string');
|
|
}
|
|
}
|
|
|
|
get digest() {
|
|
return this.value.digest;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if value is a flight data object
|
|
* @param {*} value - Value to check
|
|
* @returns {boolean} True if flight data object
|
|
*/
|
|
export function isFlightDataObj(value) {
|
|
return Array.isArray(value) &&
|
|
value.length === 4 &&
|
|
value[0] === '$' &&
|
|
typeof value[1] === 'string' &&
|
|
(value[2] === null || typeof value[2] === 'string');
|
|
}
|
|
|
|
// Map of value_class to Element class
|
|
const _types = {
|
|
'HL': HintPreload,
|
|
'I': Module,
|
|
'T': Text,
|
|
'E': Error
|
|
};
|
|
|
|
// Map of class names to Element classes
|
|
export const _tl2obj = {
|
|
'Element': Element,
|
|
'HintPreload': HintPreload,
|
|
'Module': Module,
|
|
'Text': Text,
|
|
'Data': Data,
|
|
'EmptyData': EmptyData,
|
|
'SpecialData': SpecialData,
|
|
'HTMLElement': HTMLElement,
|
|
'DataContainer': DataContainer,
|
|
'DataParent': DataParent,
|
|
'URLQuery': URLQuery,
|
|
'RSCPayload': RSCPayload,
|
|
'Error': Error
|
|
};
|
|
|
|
// Keys for dumped elements
|
|
export const _element_keys = new Set(['value', 'value_class', 'index']);
|
|
export const _dumped_element_keys = new Set([..._element_keys, 'cls']);
|
|
|
|
/**
|
|
* Resolve value to appropriate Element type
|
|
* @param {*} value - Value to resolve
|
|
* @param {string|null} value_class - Class of the value
|
|
* @param {number|null} index - Index in flight data
|
|
* @param {Function|string|null} cls - Explicit class to use
|
|
* @returns {Element} Resolved element
|
|
*/
|
|
export function resolveType(value, value_class, index, cls = null) {
|
|
// Handle already resolved elements
|
|
if (typeof value === 'object' && value !== null && 'value' in value && 'value_class' in value) {
|
|
return resolveType(value.value, value.value_class, value.index, value.cls);
|
|
}
|
|
|
|
// Handle explicit class parameter
|
|
if (cls !== null) {
|
|
if (typeof cls === 'string') {
|
|
cls = _tl2obj[cls];
|
|
}
|
|
} else {
|
|
// Determine class from value_class or value structure
|
|
if (value_class === null) {
|
|
if (Array.isArray(value)) {
|
|
const arr = value;
|
|
if (isFlightDataObj(arr)) {
|
|
if (arr[1].startsWith('$')) {
|
|
if (arr[3] === null) {
|
|
cls = Data;
|
|
} else if (typeof arr[3] === 'object' && 'buildId' in arr[3]) {
|
|
cls = RSCPayload;
|
|
} else if (typeof arr[3] === 'object' && Object.keys(arr[3]).length === 1 && 'children' in arr[3]) {
|
|
cls = DataParent;
|
|
} else {
|
|
cls = Data;
|
|
}
|
|
} else {
|
|
cls = HTMLElement;
|
|
}
|
|
} else if (arr.length === 3 && arr[2] === 'd' && arr.every(item => typeof item === 'string')) {
|
|
cls = URLQuery;
|
|
} else {
|
|
cls = DataContainer;
|
|
}
|
|
} else if (value === null) {
|
|
cls = EmptyData;
|
|
} else if (typeof value === 'object' && index === 0) {
|
|
cls = RSCPayload;
|
|
} else if (typeof value === 'string' && value.startsWith('$')) {
|
|
cls = SpecialData;
|
|
}
|
|
} else if (value_class in _types) {
|
|
cls = _types[value_class];
|
|
}
|
|
}
|
|
|
|
// Default to Element if no specific class found
|
|
if (cls === null) {
|
|
if (index === 0) {
|
|
throw new Error('Data at index 0 did not find any object to store its RSCPayload');
|
|
}
|
|
if (process?.env?.NJS_PARSER_DEBUG_TYPES) {
|
|
console.warn(`Couldn't find an appropriate type for given class \`${value_class}\`. Giving \`Element\`.`);
|
|
}
|
|
cls = Element;
|
|
}
|
|
|
|
return new cls(value, value_class, index);
|
|
}
|
|
|
|
/**
|
|
* Type lookup object (similar to Python's T class)
|
|
*/
|
|
export const T = {
|
|
Element,
|
|
HintPreload,
|
|
Module,
|
|
Text,
|
|
Data,
|
|
EmptyData,
|
|
SpecialData,
|
|
HTMLElement,
|
|
DataContainer,
|
|
DataParent,
|
|
URLQuery,
|
|
RSCPayload,
|
|
Error
|
|
};
|