njsparser/js/parser/types.js
2026-02-15 01:50:02 +01:00

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
};