diff --git a/site/scripts/api-docs-builder/src/media-element-handler.ts b/site/scripts/api-docs-builder/src/media-element-handler.ts index f81209e8..f5ce5333 100644 --- a/site/scripts/api-docs-builder/src/media-element-handler.ts +++ b/site/scripts/api-docs-builder/src/media-element-handler.ts @@ -2,29 +2,40 @@ * Media element reference extraction. * * Discovers media elements from packages/html/src/define/media/*.ts and extracts - * delegate properties, shared attributes/events/CSS vars, and slots. + * host properties, shared attributes/events/CSS vars, and slots. * * Convention: * - Define files: packages/html/src/define/media/*.ts with inline class + static tagName * - Media element classes: packages/html/src/media/{name}/index.ts - * composed as MediaPropsMixin(MediaAttachMixin(CustomMedia), Delegate) - * - Delegate classes: packages/core/src/dom/media/{name}/index.ts with getter/setter pairs. - * CustomMedia classes use inheritance mixins (e.g., HlsMediaMixin(CustomVideoElement)). + * composed as MediaAttachMixin(CustomMediaElement('video'|'audio', Host)) + * - Host classes: packages/core/src/dom/media/{name}/index.ts extending + * HTMLVideoElementHost or HTMLAudioElementHost with getter/setter pairs * - Shared data: packages/core/src/dom/media/custom-media-element/index.ts - * exports Attributes, Events, VideoCSSVars, AudioCSSVars, and template functions - * - Slots: parsed from getVideoTemplateHTML / getAudioTemplateHTML in custom-media-element + * exports CustomMediaElement factory (with static properties), VideoCSSVars, + * AudioCSSVars, and template functions + * - Slots: parsed from getVideoTemplateHTML / getCommonTemplateHTML in custom-media-element * * Exclusions (elements discovered but intentionally skipped): * - container.ts: re-exports a class, doesn't declare one inline → no static tagName found - * - background-video.ts: uses MediaAttachMixin(HTMLElement) without MediaPropsMixin → - * parseMixinChain returns null. Its API reference is manually maintained in MDX. + * - background-video.ts: uses MediaAttachMixin(HTMLElement) without CustomMediaElement → + * parseCustomMediaElementCall returns null. Its API reference is manually maintained in MDX. */ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as ts from 'typescript'; import * as tae from 'typescript-api-extractor'; import { extractCSSVars } from './css-vars-handler.js'; -import type { DelegatePropertyDef, MediaElementReference, MediaElementResult } from './pipeline.js'; +import type { HostPropertyDef, MediaElementReference, MediaElementResult } from './pipeline.js'; + +// ─── Constants ────────────────────────────────────────────────────── + +/** Classes that mark the end of the host prototype chain for property extraction. */ +const HOST_BASE_CLASSES = new Set([ + 'HTMLMediaElementHost', + 'HTMLVideoElementHost', + 'HTMLAudioElementHost', + 'EventTarget', +]); // ─── Types ─────────────────────────────────────────────────────────── @@ -33,9 +44,9 @@ interface MediaElementSource { className: string; tagName: string; mediaFilePath: string; - delegateFilePath: string; - delegateClassName: string; - customMediaClassName: string; + hostFilePath: string; + hostClassName: string; + mediaType: 'video' | 'audio'; } // ─── Module Resolution ─────────────────────────────────────────────── @@ -80,7 +91,7 @@ function discoverMediaElements(monorepoRoot: string, compilerOptions: ts.Compile /** * Parse a define/media file to extract class name, tagName, and import chain. * Returns null if the file doesn't declare an inline class with static tagName - * (container.ts) or if the class doesn't use MediaPropsMixin (background-video.ts). + * (container.ts) or if the class doesn't use CustomMediaElement (background-video.ts). */ function parseDefineFile( sourceFile: ts.SourceFile, @@ -140,15 +151,15 @@ function parseDefineFile( const mediaFilePath = resolveModuleToFile(filePath, baseImportPath, compilerOptions); if (!mediaFilePath) return null; - // Parse the media element file to find the delegate class + // Parse the media element file to find the CustomMediaElement(tag, Host) call const mediaContent = fs.readFileSync(mediaFilePath, 'utf-8'); const mediaSourceFile = ts.createSourceFile(mediaFilePath, mediaContent, ts.ScriptTarget.Latest, true); - const delegateInfo = parseMixinChain(mediaSourceFile, baseClassName); - if (!delegateInfo) return null; + const hostInfo = parseCustomMediaElementCall(mediaSourceFile, baseClassName); + if (!hostInfo) return null; - // Resolve delegate import path - let delegateImportPath: string | undefined; + // Resolve host class import path + let hostImportPath: string | undefined; ts.forEachChild(mediaSourceFile, (node) => { if (!ts.isImportDeclaration(node)) return; if (!ts.isStringLiteral(node.moduleSpecifier)) return; @@ -156,26 +167,26 @@ function parseDefineFile( if (!importClause?.namedBindings || !ts.isNamedImports(importClause.namedBindings)) return; for (const specifier of importClause.namedBindings.elements) { - if (specifier.name.text === delegateInfo.delegateClassName) { - delegateImportPath = node.moduleSpecifier.text; + if (specifier.name.text === hostInfo.hostClassName) { + hostImportPath = node.moduleSpecifier.text; break; } } }); - if (!delegateImportPath) return null; + if (!hostImportPath) return null; - const delegateFilePath = resolveModuleToFile(mediaFilePath, delegateImportPath, compilerOptions); - if (!delegateFilePath) return null; + const hostFilePath = resolveModuleToFile(mediaFilePath, hostImportPath, compilerOptions); + if (!hostFilePath) return null; return { defineFilePath: filePath, className: stripElementSuffix(className), tagName, mediaFilePath, - delegateFilePath, - delegateClassName: delegateInfo.delegateClassName, - customMediaClassName: delegateInfo.customMediaClassName, + hostFilePath, + hostClassName: hostInfo.hostClassName, + mediaType: hostInfo.mediaType, }; } @@ -184,15 +195,15 @@ function stripElementSuffix(name: string): string { } /** - * Parse the media element class to find the MediaPropsMixin(Base, Delegate) call. - * Returns null for elements that don't use MediaPropsMixin (e.g., BackgroundVideo). + * Parse the media element class to find the CustomMediaElement(tag, Host) call. + * Returns null for elements that don't use CustomMediaElement (e.g., BackgroundVideo). */ -function parseMixinChain( +function parseCustomMediaElementCall( sourceFile: ts.SourceFile, className: string -): { delegateClassName: string; customMediaClassName: string } | null { - let delegateClassName: string | undefined; - let customMediaClassName: string | undefined; +): { hostClassName: string; mediaType: 'video' | 'audio' } | null { + let hostClassName: string | undefined; + let mediaType: 'video' | 'audio' | undefined; ts.forEachChild(sourceFile, (node) => { if (!ts.isClassDeclaration(node)) return; @@ -203,60 +214,62 @@ function parseMixinChain( if (!extendsClause || extendsClause.types.length === 0) return; const extendsExpr = extendsClause.types[0]!.expression; - findMediaPropsMixin(extendsExpr); + findCustomMediaElement(extendsExpr); }); - function findMediaPropsMixin(node: ts.Node): void { - if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'MediaPropsMixin') { + function findCustomMediaElement(node: ts.Node): void { + if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 'CustomMediaElement' + ) { if (node.arguments.length >= 2) { - const delegateArg = node.arguments[1]!; - if (ts.isIdentifier(delegateArg)) { - delegateClassName = delegateArg.text; + // First arg: media type string literal ('video' or 'audio') + const tagArg = node.arguments[0]!; + if (ts.isStringLiteral(tagArg)) { + mediaType = tagArg.text === 'audio' ? 'audio' : 'video'; + } + // Second arg: host class identifier + const hostArg = node.arguments[1]!; + if (ts.isIdentifier(hostArg)) { + hostClassName = hostArg.text; } - const baseArg = node.arguments[0]!; - customMediaClassName = unwrapMixinBase(baseArg); } return; } - ts.forEachChild(node, findMediaPropsMixin); + ts.forEachChild(node, findCustomMediaElement); } - if (!delegateClassName || !customMediaClassName) return null; - return { delegateClassName, customMediaClassName }; + if (!hostClassName || !mediaType) return null; + return { hostClassName, mediaType }; } -function unwrapMixinBase(node: ts.Node): string | undefined { - if (ts.isIdentifier(node)) return node.text; - if (ts.isCallExpression(node) && node.arguments.length > 0) { - return unwrapMixinBase(node.arguments[0]!); - } - return undefined; -} - -// ─── Delegate Property Extraction ──────────────────────────────────── +// ─── Host Property Extraction ─────────────────────────────────────── /** - * Extract getter/setter pairs from a delegate class and its ancestors, - * mirroring what buildAttrPropMap() in media-props-mixin.ts does at runtime. + * Extract getter/setter pairs from a host class and its ancestors, + * mirroring what CustomMediaElement does at runtime when it walks + * the MediaHost prototype chain. */ -function extractDelegateProperties( +function extractHostProperties( filePath: string, - delegateClassName: string, + hostClassName: string, compilerOptions: ts.CompilerOptions -): Record { - const properties: Record = {}; - extractClassProperties(filePath, delegateClassName, properties, compilerOptions, new Set()); +): Record { + const properties: Record = {}; + extractClassProperties(filePath, hostClassName, properties, compilerOptions, new Set()); return properties; } /** * Recursively extract getter/setter pairs from a class and its parent chain. * Child properties override parent properties (checked via the `seen` set). + * Stops at host base classes (HTMLMediaElementHost, HTMLVideoElementHost, etc.). */ function extractClassProperties( filePath: string, className: string, - properties: Record, + properties: Record, compilerOptions: ts.CompilerOptions, seen: Set ): void { @@ -274,7 +287,7 @@ function extractClassProperties( ts.forEachChild(sourceFile, (node) => { if (!ts.isClassDeclaration(node) || !node.name || node.name.text !== className) return; - // Check for extends clause (delegate inheritance) + // Check for extends clause (host inheritance) if (node.heritageClauses) { const extendsClause = node.heritageClauses.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword); if (extendsClause && extendsClause.types.length > 0) { @@ -308,7 +321,7 @@ function extractClassProperties( }); // Resolve parent class and extract its properties first (child overrides parent) - if (parentClassName && parentClassName !== 'EventTarget') { + if (parentClassName && !HOST_BASE_CLASSES.has(parentClassName)) { // Find the import for the parent class ts.forEachChild(sourceFile, (node) => { if (!ts.isImportDeclaration(node)) return; @@ -339,7 +352,7 @@ function extractClassProperties( // Apply this class's properties (overrides parent) for (const [name, info] of getters) { - const def: DelegatePropertyDef = { + const def: HostPropertyDef = { type: info.type, readonly: !setters.has(name), }; @@ -371,31 +384,56 @@ function getJSDocDescription(node: ts.Node): string | undefined { // ─── Shared Data Extraction ────────────────────────────────────────── -function extractStringArray(filePath: string, varName: string): string[] { +/** + * Extract native attribute names from the `static properties` object inside + * the CustomMediaElement factory. Each key maps to an attribute name via + * `props[key].attribute ?? key.toLowerCase()`. + */ +function extractStaticProperties(filePath: string): string[] { const content = fs.readFileSync(filePath, 'utf-8'); const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); - const items: string[] = []; + const attributes: string[] = []; - ts.forEachChild(sourceFile, (node) => { - if (!ts.isVariableStatement(node)) return; - for (const decl of node.declarationList.declarations) { - if (!ts.isIdentifier(decl.name) || decl.name.text !== varName) continue; - if (!decl.initializer) continue; + function visit(node: ts.Node): void { + // Look for: static properties = { ... } + if ( + ts.isPropertyDeclaration(node) && + node.name && + ts.isIdentifier(node.name) && + node.name.text === 'properties' && + node.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) && + node.initializer && + ts.isObjectLiteralExpression(node.initializer) + ) { + for (const prop of node.initializer.properties) { + if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue; - let expr = decl.initializer; - if (ts.isAsExpression(expr)) expr = expr.expression; + const propName = prop.name.text; + let attrName = propName.toLowerCase(); - if (ts.isArrayLiteralExpression(expr)) { - for (const el of expr.elements) { - if (ts.isStringLiteral(el)) { - items.push(el.text); + // Check for explicit `attribute` override in the property config + if (ts.isObjectLiteralExpression(prop.initializer)) { + for (const configProp of prop.initializer.properties) { + if ( + ts.isPropertyAssignment(configProp) && + ts.isIdentifier(configProp.name) && + configProp.name.text === 'attribute' && + ts.isStringLiteral(configProp.initializer) + ) { + attrName = configProp.initializer.text; + } } } - } - } - }); - return items; + attributes.push(attrName); + } + return; + } + ts.forEachChild(node, visit); + } + + visit(sourceFile); + return attributes; } function extractSlotsFromTemplate(filePath: string, templateFnName: string): string[] { @@ -418,6 +456,47 @@ function extractSlotsFromTemplate(filePath: string, templateFnName: string): str return slots; } +/** + * Extract slots from getCommonTemplateHTML — a factory function that returns + * a function containing the template string. + */ +function extractSlotsFromTemplateFactory(filePath: string, factoryFnName: string): string[] { + const content = fs.readFileSync(filePath, 'utf-8'); + const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); + const slots: string[] = []; + + function visit(node: ts.Node): void { + if (ts.isFunctionDeclaration(node) && node.name?.text === factoryFnName && node.body) { + // The factory returns a function — look for a return statement with a function/arrow + for (const stmt of node.body.statements) { + if (ts.isReturnStatement(stmt) && stmt.expression) { + // Could be an arrow function or function expression + let innerBody: ts.Block | ts.Expression | undefined; + if (ts.isArrowFunction(stmt.expression)) { + innerBody = stmt.expression.body; + } else if (ts.isFunctionExpression(stmt.expression)) { + innerBody = stmt.expression.body; + } + + if (innerBody) { + const templateText = ts.isBlock(innerBody) + ? extractTemplateString(innerBody) + : getTemplateText(innerBody as ts.Expression); + if (templateText) { + parseSlots(templateText, slots); + } + } + } + } + return; + } + ts.forEachChild(node, visit); + } + + visit(sourceFile); + return slots; +} + function extractTemplateString(block: ts.Block): string | undefined { for (const stmt of block.statements) { if (ts.isReturnStatement(stmt) && stmt.expression) { @@ -453,63 +532,68 @@ function parseSlots(html: string, slots: string[]): void { } } +// ─── Event Extraction ──────────────────────────────────────────────── + /** - * Determine whether a CustomMedia base class is video or audio by checking - * the extends clause of the class that defines it (e.g., HlsMediaMixin(CustomVideoElement)). + * Extract event names from a composite event interface (e.g. VideoEvents, AudioEvents) + * by walking its `extends` chain and collecting property keys from each parent interface. + * + * Convention: capability event interfaces (MediaPlaybackEvents, etc.) are flat + * `eventName: EventLike` maps, and VideoEvents/AudioEvents compose them via `extends`. */ -function resolveMediaType( - mediaFilePath: string, - customMediaClassName: string, - compilerOptions: ts.CompilerOptions -): 'video' | 'audio' { - const content = fs.readFileSync(mediaFilePath, 'utf-8'); - const sourceFile = ts.createSourceFile(mediaFilePath, content, ts.ScriptTarget.Latest, true); +function extractEventsFromTypes(filePath: string, interfaceName: string): string[] { + const content = fs.readFileSync(filePath, 'utf-8'); + const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true); + + // Build a map of interface name → { extends list, own property keys } + const interfaces = new Map(); - let importSource: string | undefined; ts.forEachChild(sourceFile, (node) => { - if (!ts.isImportDeclaration(node)) return; - if (!ts.isStringLiteral(node.moduleSpecifier)) return; - const importClause = node.importClause; - if (!importClause?.namedBindings || !ts.isNamedImports(importClause.namedBindings)) return; + if (!ts.isInterfaceDeclaration(node) || !node.name) return; - for (const specifier of importClause.namedBindings.elements) { - if (specifier.name.text === customMediaClassName) { - importSource = node.moduleSpecifier.text; - break; + const name = node.name.text; + const extendsList: string[] = []; + const keys: string[] = []; + + if (node.heritageClauses) { + for (const clause of node.heritageClauses) { + if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue; + for (const type of clause.types) { + if (ts.isIdentifier(type.expression)) { + extendsList.push(type.expression.text); + } + } } } - }); - // Fallback: default to video (all current media elements are video-based) - if (!importSource) return 'video'; - - const resolvedPath = resolveModuleToFile(mediaFilePath, importSource, compilerOptions); - if (!resolvedPath) return 'video'; - - const sourceContent = fs.readFileSync(resolvedPath, 'utf-8'); - const resolvedSourceFile = ts.createSourceFile(resolvedPath, sourceContent, ts.ScriptTarget.Latest, true); - - // Check the extends clause for CustomAudioElement specifically - let mediaType: 'video' | 'audio' = 'video'; - ts.forEachChild(resolvedSourceFile, (node) => { - if (!ts.isClassDeclaration(node) || node.name?.text !== customMediaClassName) return; - if (!node.heritageClauses) return; - - const extendsClause = node.heritageClauses.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword); - if (!extendsClause || extendsClause.types.length === 0) return; - - // Walk the extends expression looking for CustomAudioElement identifier - function checkForAudio(n: ts.Node): void { - if (ts.isIdentifier(n) && n.text === 'CustomAudioElement') { - mediaType = 'audio'; - return; + for (const member of node.members) { + if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) { + keys.push(member.name.text); } - ts.forEachChild(n, checkForAudio); } - checkForAudio(extendsClause.types[0]!.expression); + + interfaces.set(name, { extends: extendsList, keys }); }); - return mediaType; + // Recursively collect keys from the target interface and all ancestors + const events: string[] = []; + const visited = new Set(); + + function collect(name: string): void { + if (visited.has(name)) return; + visited.add(name); + + const iface = interfaces.get(name); + if (!iface) return; + + for (const parent of iface.extends) { + collect(parent); + } + events.push(...iface.keys); + } + + collect(interfaceName); + return events; } // ─── Pipeline ──────────────────────────────────────────────────────── @@ -527,8 +611,12 @@ export function generateMediaElementReferences(monorepoRoot: string): MediaEleme if (!fs.existsSync(customMediaPath)) return []; // Read shared data - const allAttributes = extractStringArray(customMediaPath, 'Attributes'); - const allEvents = extractStringArray(customMediaPath, 'Events'); + const allAttributes = extractStaticProperties(customMediaPath); + + // Extract events from capability contract types + const mediaTypesPath = path.join(monorepoRoot, 'packages/core/src/core/media/types.ts'); + const videoEvents = fs.existsSync(mediaTypesPath) ? extractEventsFromTypes(mediaTypesPath, 'VideoEvents') : []; + const audioEvents = fs.existsSync(mediaTypesPath) ? extractEventsFromTypes(mediaTypesPath, 'AudioEvents') : []; // Extract CSS vars using the existing handler (needs a TS program) const program = ts.createProgram([customMediaPath], compilerOptions); @@ -551,35 +639,30 @@ export function generateMediaElementReferences(monorepoRoot: string): MediaEleme // Extract slots from template functions const videoSlots = extractSlotsFromTemplate(customMediaPath, 'getVideoTemplateHTML'); - const audioSlots = extractSlotsFromTemplate(customMediaPath, 'getAudioTemplateHTML'); + const audioSlots = extractSlotsFromTemplateFactory(customMediaPath, 'getCommonTemplateHTML'); const results: MediaElementResult[] = []; for (const source of sources) { - const delegateProperties = extractDelegateProperties( - source.delegateFilePath, - source.delegateClassName, - compilerOptions - ); + const hostProperties = extractHostProperties(source.hostFilePath, source.hostClassName, compilerOptions); - const mediaType = resolveMediaType(source.mediaFilePath, source.customMediaClassName, compilerOptions); - - // Deduplicate: delegate props that overlap with native Attributes - const delegateAttrNames = new Set(); - for (const propName of Object.keys(delegateProperties)) { - delegateAttrNames.add(propName.toLowerCase()); + // Deduplicate: host props that overlap with native attributes + const hostAttrNames = new Set(); + for (const propName of Object.keys(hostProperties)) { + hostAttrNames.add(propName.toLowerCase()); } - const nativeAttributes = allAttributes.filter((attr) => !delegateAttrNames.has(attr)); + const nativeAttributes = allAttributes.filter((attr) => !hostAttrNames.has(attr)); - const cssCustomProperties = mediaType === 'video' ? videoCSSVars : audioCSSVars; - const slots = mediaType === 'video' ? videoSlots : audioSlots; + const cssCustomProperties = source.mediaType === 'video' ? videoCSSVars : audioCSSVars; + const slots = source.mediaType === 'video' ? videoSlots : audioSlots; + const events = source.mediaType === 'video' ? videoEvents : audioEvents; const reference: MediaElementReference = { name: source.className, tagName: source.tagName, - delegateProperties, + hostProperties, nativeAttributes, - events: [...allEvents], + events, cssCustomProperties, slots, }; diff --git a/site/scripts/api-docs-builder/src/pipeline.ts b/site/scripts/api-docs-builder/src/pipeline.ts index 744958a4..0f3d942a 100644 --- a/site/scripts/api-docs-builder/src/pipeline.ts +++ b/site/scripts/api-docs-builder/src/pipeline.ts @@ -575,7 +575,7 @@ export { generatePresetReferences } from './preset-handler.js'; // MEDIA ELEMENT REFERENCE PIPELINE // ═══════════════════════════════════════════════════════════════════════ -export interface DelegatePropertyDef { +export interface HostPropertyDef { type: string; description?: string; readonly: boolean; @@ -584,7 +584,7 @@ export interface DelegatePropertyDef { export interface MediaElementReference { name: string; tagName: string; - delegateProperties: Record; + hostProperties: Record; nativeAttributes: string[]; events: string[]; cssCustomProperties: Record; diff --git a/site/scripts/api-docs-builder/src/tests/e2e.test.ts b/site/scripts/api-docs-builder/src/tests/e2e.test.ts index 43728e9b..f114cd01 100644 --- a/site/scripts/api-docs-builder/src/tests/e2e.test.ts +++ b/site/scripts/api-docs-builder/src/tests/e2e.test.ts @@ -62,21 +62,21 @@ * * Media elements (packages/html/src/define/media/ + packages/core/src/dom/media/): * simple-video — Simple media element. Exercises: discovery via static - * tagName in define/media/*.ts, minimal delegate (src rw, - * engine readonly), shared Attributes/Events/CSS vars + * tagName in define/media/*.ts, minimal host (src rw, + * engine readonly), shared attributes/events/CSS vars * from custom-media-element, slots parsed from template HTML. - * complex-video — Complex media element. Exercises: delegate with JSDoc + * complex-video — Complex media element. Exercises: host with JSDoc * descriptions, multiple property types (string, boolean, - * Record), delegate-vs-native attribute deduplication - * (src, preload in delegate → omitted from nativeAttributes). - * extending-video — Extending media element. Exercises: delegate inheritance - * (ExtendingDelegate extends ComplexDelegate). Builder must + * Record), host-vs-native attribute deduplication + * (src, preload in host → omitted from nativeAttributes). + * extending-video — Extending media element. Exercises: host inheritance + * (ExtendingHost extends ComplexHost). Builder must * walk the extends chain to include inherited properties. * Child overrides (debug) replace parent definitions. * container.ts — Exclusion case. Not a media element — re-exports an * existing class instead of declaring one inline. * background-video.ts — Exclusion case. Uses MediaAttachMixin(HTMLElement) - * without MediaPropsMixin. API reference manually maintained. + * without CustomMediaElement. API reference manually maintained. */ import * as path from 'node:path'; import { describe, expect, it } from 'vitest'; @@ -988,25 +988,25 @@ describe('Preset pipeline (end-to-end)', () => { // ═══════════════════════════════════════════════════════════════════════ // // Media elements are custom elements that wrap native