feat(core): lock fullscreen orientation (#1656)

This commit is contained in:
Sam Potts
2026-06-10 10:32:43 +10:00
committed by GitHub
parent 3d2225e77e
commit 62d0524b83
20 changed files with 770 additions and 13 deletions
@@ -12,6 +12,7 @@
* - Feature files: *.ts in the features directory (excluding index, presets, feature.parts)
* - Feature exports: const matching *Feature (singular, not *Features)
* - State type: explicit return type annotation on the state() arrow function
* - Silent features: state() returns an empty object
* - State interfaces: exported from packages/core/src/core/media/state.ts
*/
import * as fs from 'node:fs';
@@ -25,7 +26,7 @@ const SKIP_FILES = new Set(['index.ts', 'presets.ts', 'feature.parts.ts']);
interface FeatureSource {
filePath: string;
name: string;
stateTypeName: string;
stateTypeName?: string;
}
// ─── Discovery ────────────────────────────────────────────────────
@@ -54,6 +55,7 @@ function discoverFeatureSources(featuresDir: string): FeatureSource[] {
let name: string | undefined;
let stateTypeName: string | undefined;
let silent = false;
for (const prop of arg.properties) {
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
@@ -66,11 +68,13 @@ function discoverFeatureSources(featuresDir: string): FeatureSource[] {
const fn = prop.initializer;
if ((ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) && fn.type && ts.isTypeReferenceNode(fn.type)) {
stateTypeName = fn.type.typeName.getText(sourceFile);
} else if (isEmptyState(fn)) {
silent = true;
}
}
}
if (name && stateTypeName) {
if (name && (stateTypeName || silent)) {
sources.push({ filePath, name, stateTypeName });
}
}
@@ -80,6 +84,24 @@ function discoverFeatureSources(featuresDir: string): FeatureSource[] {
return sources;
}
function isEmptyState(node: ts.Expression): boolean {
if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) return false;
if (ts.isBlock(node.body)) return false;
const body = unwrapParentheses(node.body);
return ts.isObjectLiteralExpression(body) && body.properties.length === 0;
}
function unwrapParentheses(node: ts.Expression): ts.Expression {
let expression = node;
while (ts.isParenthesizedExpression(expression)) {
expression = expression.expression;
}
return expression;
}
// ─── Type Formatting ──────────────────────────────────────────────
function formatCheckerType(type: ts.Type, checker: ts.TypeChecker): string {
@@ -194,6 +216,18 @@ export function generateFeatureReferences(monorepoRoot: string): FeatureResult[]
const results: FeatureResult[] = [];
for (const source of sources) {
if (!source.stateTypeName) {
const ref: FeatureReference = {
name: source.name,
slug: source.name,
state: {},
actions: {},
};
results.push({ name: source.name, slug: source.name, reference: ref });
continue;
}
const interfaceDecl = interfaces.get(source.stateTypeName);
if (!interfaceDecl) continue;
@@ -713,6 +713,7 @@ describe('Feature pipeline (end-to-end)', () => {
describe('Discovery', () => {
it('discovers features from the features index', () => {
const names = results.map((r) => r.name);
expect(names).toContain('orientationLock');
expect(names).toContain('playback');
expect(names).toContain('volume');
});
@@ -729,7 +730,16 @@ describe('Feature pipeline (end-to-end)', () => {
});
it('produces one result per feature', () => {
expect(results.length).toBe(2);
expect(results.length).toBe(3);
});
});
describe('orientationLock (silent feature)', () => {
it('generates an empty reference for empty state', () => {
const ref = findFeature('orientationLock')!.reference;
expect(ref.state).toEqual({});
expect(ref.actions).toEqual({});
});
});
@@ -4,5 +4,7 @@
* Exercises: namespace re-export filtering `export * as features from './feature.parts'`
* in the index should NOT produce a feature entry named "features".
*/
export { orientationLockFeature as orientationLock } from './orientation-lock';
export { playbackFeature as playback } from './playback';
export { volumeFeature as volume } from './volume';
@@ -7,6 +7,7 @@
*/
export * as features from './feature.parts';
export * from './orientation-lock';
export * from './playback';
export * from './presets';
export * from './volume';
@@ -0,0 +1,6 @@
import { definePlayerFeature } from '../../feature';
export const orientationLockFeature = definePlayerFeature({
name: 'orientationLock',
state: () => ({}),
});