mirror of
https://github.com/zoriya/v10.git
synced 2026-08-13 09:30:38 +00:00
refactor(compiler)!: move component generation to core
This commit is contained in:
@@ -15,14 +15,6 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./jsx-runtime": {
|
||||
"types": "./dist/jsx-runtime.d.ts",
|
||||
"default": "./dist/jsx-runtime.js"
|
||||
},
|
||||
"./jsx-dev-runtime": {
|
||||
"types": "./dist/jsx-dev-runtime.d.ts",
|
||||
"default": "./dist/jsx-dev-runtime.js"
|
||||
},
|
||||
"./vite": {
|
||||
"types": "./dist/plugins/vite.d.ts",
|
||||
"default": "./dist/plugins/vite.js"
|
||||
@@ -46,15 +38,13 @@
|
||||
"./tailwind": {
|
||||
"types": "./dist/tailwind/index.d.ts",
|
||||
"default": "./dist/tailwind/index.js"
|
||||
},
|
||||
"./tsconfig.preset.json": "./tsconfig.preset.json"
|
||||
}
|
||||
},
|
||||
"bin": {
|
||||
"vjs": "./dist/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"tsconfig.preset.json"
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
@@ -80,7 +70,6 @@
|
||||
"typescript": "^6.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@videojs/core": "workspace:*",
|
||||
"tsdown": "^0.21.4",
|
||||
"vite": "^8.0.0",
|
||||
"vitest": "^4.1.0"
|
||||
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
formatCompilerDiagnosticJsonLine,
|
||||
formatDiagnosticSummaryJsonLine,
|
||||
} from './diagnostics';
|
||||
import { generate } from './generate';
|
||||
import { CONFIG_FILENAMES, loadConfig } from './load-config';
|
||||
import { loadConfig } from './load-config';
|
||||
|
||||
interface ParsedArgs {
|
||||
command: string | undefined;
|
||||
@@ -54,7 +53,6 @@ function printHelp(): void {
|
||||
'Usage: vjs <command> [options]',
|
||||
'',
|
||||
'Commands:',
|
||||
' generate Generate components from the configured manifests',
|
||||
' compile <file> Compile a JSX file',
|
||||
'',
|
||||
'Options:',
|
||||
@@ -67,18 +65,6 @@ function printHelp(): void {
|
||||
);
|
||||
}
|
||||
|
||||
async function runGenerate(configOverride: string | undefined): Promise<void> {
|
||||
const cwd = process.cwd();
|
||||
const loaded = await loadConfig(cwd, configOverride);
|
||||
if (!loaded) {
|
||||
throw new Error(
|
||||
`No compiler config found in ${cwd}. Expected one of: ${CONFIG_FILENAMES.join(', ')}, or pass --config <path>.`
|
||||
);
|
||||
}
|
||||
const result = await generate(loaded.config);
|
||||
process.stdout.write(`Wrote ${result.outputPath}\n`);
|
||||
}
|
||||
|
||||
async function runCompile(
|
||||
positional: readonly string[],
|
||||
configOverride: string | undefined,
|
||||
@@ -128,9 +114,6 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case 'generate':
|
||||
await runGenerate(configOverride);
|
||||
return;
|
||||
case 'compile':
|
||||
await runCompile(positional, configOverride, outFile, diagnosticsFormat);
|
||||
return;
|
||||
|
||||
@@ -41,30 +41,6 @@ export interface StylePipeline {
|
||||
setup(context: CompilerContext): CompilerPipelineStep | Promise<CompilerPipelineStep>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk-defined component entry. Globs `files`, derives each component's name
|
||||
* from the filename (extension stripped) via `name(stem)`, and inline-emits
|
||||
* `createComponent({ name })` calls. Components defined this way are
|
||||
* BaseProps-only — to type Props, parts, or partProps, use a manifest file.
|
||||
*/
|
||||
export interface BulkComponentEntry {
|
||||
files: string;
|
||||
name: (filename: string) => string;
|
||||
}
|
||||
|
||||
export type ComponentEntry = string | BulkComponentEntry;
|
||||
|
||||
export interface GenerateConfig {
|
||||
/**
|
||||
* Component sources. Each entry is either:
|
||||
* - a glob string matching `*-component.ts` manifest files, or
|
||||
* - a `{ files, name }` object that bulk-defines components from arbitrary files.
|
||||
*/
|
||||
components: readonly ComponentEntry[];
|
||||
/** Path the generator writes the components file to. */
|
||||
output: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-target compile configuration. Currently only `react` is shipped, but
|
||||
* the shape is extensible for `html`/etc.
|
||||
@@ -84,7 +60,6 @@ export interface CompilerTarget {
|
||||
|
||||
export interface CompilerConfig {
|
||||
files?: readonly string[] | undefined;
|
||||
generate?: GenerateConfig;
|
||||
target?: CompilerTarget | undefined;
|
||||
styles?: StylePipeline | undefined;
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
declare const __PROPS_BRAND__: unique symbol;
|
||||
|
||||
export interface ComponentManifest<
|
||||
Props extends object = Record<string, never>,
|
||||
Parts extends readonly string[] = readonly string[],
|
||||
PartProps extends Partial<Record<Parts[number], object>> = Partial<Record<Parts[number], object>>,
|
||||
> {
|
||||
name: string;
|
||||
parts?: Parts;
|
||||
dataAttrs?: Record<string, string>;
|
||||
partProps?: PartProps;
|
||||
readonly [__PROPS_BRAND__]?: Props;
|
||||
}
|
||||
|
||||
export type InferProps<T> =
|
||||
T extends ComponentManifest<infer P, readonly string[], Partial<Record<string, object>>> ? P : never;
|
||||
|
||||
export type InferParts<T> =
|
||||
T extends ComponentManifest<object, infer Parts, Partial<Record<string, object>>>
|
||||
? readonly string[] extends Parts
|
||||
? never
|
||||
: Parts[number]
|
||||
: never;
|
||||
|
||||
export type InferPartProps<T, K extends string> =
|
||||
T extends ComponentManifest<object, readonly string[], infer PartProps>
|
||||
? K extends keyof PartProps
|
||||
? PartProps[K]
|
||||
: never
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Define a component manifest.
|
||||
*
|
||||
* Curried so the `Props` generic can be supplied without disabling inference
|
||||
* of `Parts` and `PartProps` from the manifest body:
|
||||
*
|
||||
* @example
|
||||
* const Slider = defineComponent<SliderProps>()({
|
||||
* name: 'Slider',
|
||||
* parts: SliderParts,
|
||||
* dataAttrs: SliderDataAttrs,
|
||||
* });
|
||||
*
|
||||
* const Controls = defineComponent()({
|
||||
* name: 'Controls',
|
||||
* parts: ControlsParts,
|
||||
* dataAttrs: ControlsDataAttrs,
|
||||
* });
|
||||
*/
|
||||
export function defineComponent<Props extends object = Record<string, never>>() {
|
||||
return <
|
||||
const Parts extends readonly string[] = readonly string[],
|
||||
const PartProps extends Partial<Record<Parts[number], object>> = Partial<Record<Parts[number], object>>,
|
||||
>(
|
||||
manifest: Omit<ComponentManifest<Props, Parts, PartProps>, typeof __PROPS_BRAND__>
|
||||
): ComponentManifest<Props, Parts, PartProps> => manifest as ComponentManifest<Props, Parts, PartProps>;
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import { existsSync, globSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { basename, dirname, extname, isAbsolute, relative, resolve } from 'node:path';
|
||||
import ts from 'typescript';
|
||||
|
||||
import type { BulkComponentEntry, CompilerConfig } from './config';
|
||||
|
||||
interface ManifestComponent {
|
||||
kind: 'manifest';
|
||||
name: string;
|
||||
/** Module specifier (relative to output) for `import XDef from '...'`. */
|
||||
manifestFrom: string;
|
||||
}
|
||||
|
||||
interface InlineComponent {
|
||||
kind: 'inline';
|
||||
name: string;
|
||||
}
|
||||
|
||||
type ResolvedComponent = ManifestComponent | InlineComponent;
|
||||
|
||||
function isDefineComponentCall(node: ts.Node): node is ts.CallExpression {
|
||||
if (!ts.isCallExpression(node)) return false;
|
||||
const callee = node.expression;
|
||||
if (ts.isIdentifier(callee) && callee.text === 'defineComponent') return true;
|
||||
// Curried form: defineComponent<P>()(...) — outer call's expression is the inner call.
|
||||
return (
|
||||
ts.isCallExpression(callee) && ts.isIdentifier(callee.expression) && callee.expression.text === 'defineComponent'
|
||||
);
|
||||
}
|
||||
|
||||
function findDefaultExportCall(sourceFile: ts.SourceFile): ts.CallExpression | null {
|
||||
for (const stmt of sourceFile.statements) {
|
||||
if (!ts.isExportAssignment(stmt) || stmt.isExportEquals) continue;
|
||||
if (isDefineComponentCall(stmt.expression)) return stmt.expression;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseComponentName(manifestPath: string): string {
|
||||
const sourceText = readFileSync(manifestPath, 'utf8');
|
||||
const sourceFile = ts.createSourceFile(manifestPath, sourceText, ts.ScriptTarget.Latest, true);
|
||||
const call = findDefaultExportCall(sourceFile);
|
||||
if (!call) {
|
||||
throw new Error(`No \`export default defineComponent(...)\` found in ${manifestPath}`);
|
||||
}
|
||||
const arg = call.arguments[0];
|
||||
if (!arg || !ts.isObjectLiteralExpression(arg)) {
|
||||
throw new Error(`defineComponent() in ${manifestPath} must take an object literal`);
|
||||
}
|
||||
for (const prop of arg.properties) {
|
||||
if (
|
||||
ts.isPropertyAssignment(prop) &&
|
||||
ts.isIdentifier(prop.name) &&
|
||||
prop.name.text === 'name' &&
|
||||
ts.isStringLiteral(prop.initializer)
|
||||
) {
|
||||
return prop.initializer.text;
|
||||
}
|
||||
}
|
||||
throw new Error(`defineComponent() in ${manifestPath} is missing a literal \`name:\` field`);
|
||||
}
|
||||
|
||||
function manifestPathToImport(manifestPath: string, outputFile: string): string {
|
||||
let rel = relative(dirname(outputFile), manifestPath);
|
||||
if (!rel.startsWith('.')) rel = `./${rel}`;
|
||||
return rel.replace(/\.ts$/, '');
|
||||
}
|
||||
|
||||
function fileStem(filePath: string): string {
|
||||
const base = basename(filePath);
|
||||
const ext = extname(base);
|
||||
return ext ? base.slice(0, -ext.length) : base;
|
||||
}
|
||||
|
||||
function resolveManifestEntry(pattern: string, cwd: string, outputAbsolute: string): ManifestComponent[] {
|
||||
const matches = globSync(pattern, { cwd }).map((p) => (isAbsolute(p) ? p : resolve(cwd, p)));
|
||||
return matches.map((manifestPath) => ({
|
||||
kind: 'manifest',
|
||||
name: parseComponentName(manifestPath),
|
||||
manifestFrom: manifestPathToImport(manifestPath, outputAbsolute),
|
||||
}));
|
||||
}
|
||||
|
||||
function resolveBulkEntry(entry: BulkComponentEntry, cwd: string): InlineComponent[] {
|
||||
const matches = globSync(entry.files, { cwd });
|
||||
return matches.map((file) => ({
|
||||
kind: 'inline',
|
||||
name: entry.name(fileStem(file)),
|
||||
}));
|
||||
}
|
||||
|
||||
function emitHeader(entries: readonly ResolvedComponent[]): string {
|
||||
const manifestLines = entries
|
||||
.filter((e): e is ManifestComponent => e.kind === 'manifest')
|
||||
.sort((a, b) => compareImportSpecifiers(a.manifestFrom, b.manifestFrom))
|
||||
.map((e) => `import ${e.name}Def from '${e.manifestFrom}';`)
|
||||
.join('\n');
|
||||
const head = `// AUTO-GENERATED by \`@videojs/compiler\`. DO NOT EDIT.
|
||||
import { createComponent } from '@videojs/compiler/jsx-runtime';`;
|
||||
return manifestLines ? `${head}\n\n${manifestLines}` : head;
|
||||
}
|
||||
|
||||
function manifestRef(entry: ResolvedComponent): string {
|
||||
return entry.kind === 'manifest' ? `${entry.name}Def` : `{ name: '${entry.name}' }`;
|
||||
}
|
||||
|
||||
function compareImportSpecifiers(a: string, b: string): number {
|
||||
const aKey = a.replaceAll('/', ' ');
|
||||
const bKey = b.replaceAll('/', ' ');
|
||||
if (aKey < bKey) return -1;
|
||||
if (aKey > bKey) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function emitComponents(entries: readonly ResolvedComponent[]): string {
|
||||
return entries.map((e) => `export const ${e.name} = createComponent(${manifestRef(e)});`).join('\n');
|
||||
}
|
||||
|
||||
function emitMetadata(entries: readonly ResolvedComponent[]): string {
|
||||
const lines = entries.map((e) => ` ${e.name}: ${manifestRef(e)},`);
|
||||
return `export const COMPONENTS = {\n${lines.join('\n')}\n} as const;
|
||||
|
||||
export type Components = typeof COMPONENTS;`;
|
||||
}
|
||||
|
||||
export interface GenerateResult {
|
||||
outputPath: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export async function generate(config: CompilerConfig): Promise<GenerateResult> {
|
||||
if (!config.generate) {
|
||||
throw new Error('@videojs/compiler: generate() requires a `generate` field in the compiler config');
|
||||
}
|
||||
|
||||
const { components, output } = config.generate;
|
||||
const cwd = process.cwd();
|
||||
const outputAbsolute = isAbsolute(output) ? output : resolve(cwd, output);
|
||||
|
||||
const resolved: ResolvedComponent[] = components.flatMap<ResolvedComponent>((entry) =>
|
||||
typeof entry === 'string' ? resolveManifestEntry(entry, cwd, outputAbsolute) : resolveBulkEntry(entry, cwd)
|
||||
);
|
||||
const entries = resolved.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
if (entries.length === 0) {
|
||||
throw new Error(`No component sources matched: ${JSON.stringify(components)}`);
|
||||
}
|
||||
|
||||
const source = `${[emitHeader(entries), emitComponents(entries), emitMetadata(entries)].join('\n\n')}\n`;
|
||||
// Skip the write when contents are unchanged so watch-mode rebuilds don't
|
||||
// re-trigger themselves.
|
||||
const existing = existsSync(outputAbsolute) ? readFileSync(outputAbsolute, 'utf8') : null;
|
||||
|
||||
if (existing !== source) {
|
||||
writeFileSync(outputAbsolute, source, 'utf8');
|
||||
}
|
||||
|
||||
return { outputPath: outputAbsolute, source };
|
||||
}
|
||||
@@ -12,13 +12,6 @@ export {
|
||||
react,
|
||||
type StylePipeline,
|
||||
} from './config';
|
||||
export {
|
||||
type ComponentManifest,
|
||||
defineComponent,
|
||||
type InferPartProps,
|
||||
type InferParts,
|
||||
type InferProps,
|
||||
} from './define-component';
|
||||
export {
|
||||
compilerDiagnosticToJsonEvent,
|
||||
type DiagnosticFormat,
|
||||
@@ -38,6 +31,14 @@ export {
|
||||
mapLogLevelToString,
|
||||
shouldUseColor,
|
||||
} from './diagnostics';
|
||||
export { type GenerateResult, generate } from './generate';
|
||||
export { type TailwindMode, type TailwindOptions, tailwind } from './tailwind';
|
||||
export type { ImportRef, ImportRule } from './transforms/imports';
|
||||
export {
|
||||
accessPath,
|
||||
type JsxChildReplacement,
|
||||
jsxExpression,
|
||||
propertyAccess,
|
||||
type ReplaceJsxChildOptions,
|
||||
readStringAttribute,
|
||||
replaceJsxChild,
|
||||
} from './transforms/jsx';
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './jsx-runtime';
|
||||
export { jsx as jsxDEV } from './jsx-runtime';
|
||||
@@ -1,100 +0,0 @@
|
||||
import type { ComponentManifest, InferPartProps, InferParts, InferProps } from './define-component';
|
||||
|
||||
export const VIDEOJS_NODE = Symbol.for('@videojs/node');
|
||||
|
||||
export type ComponentType = string | Component<never> | typeof Fragment;
|
||||
|
||||
export interface ComponentNode {
|
||||
readonly [VIDEOJS_NODE]: true;
|
||||
readonly type: ComponentType;
|
||||
readonly props: Record<string, unknown>;
|
||||
readonly key: string | number | null;
|
||||
}
|
||||
|
||||
export interface BaseProps {
|
||||
className?: string | undefined;
|
||||
children?: unknown;
|
||||
}
|
||||
|
||||
export interface Component<Props extends object> {
|
||||
(props: BaseProps & Props): ComponentNode;
|
||||
readonly $$component: { name: string; part: string | null };
|
||||
}
|
||||
|
||||
type PartComponentProps<M, K extends string> = K extends 'Root'
|
||||
? InferProps<M>
|
||||
: [NonNullable<InferPartProps<M, K>>] extends [never]
|
||||
? Record<string, never>
|
||||
: NonNullable<InferPartProps<M, K>>;
|
||||
|
||||
type CompoundComponent<M> = {
|
||||
[K in InferParts<M> & string]: Component<PartComponentProps<M, K>>;
|
||||
};
|
||||
|
||||
export type CreateComponentResult<M> = [InferParts<M>] extends [never]
|
||||
? Component<InferProps<M>>
|
||||
: CompoundComponent<M>;
|
||||
|
||||
function makePart<Props extends object>(name: string, part: string | null): Component<Props> {
|
||||
const fn = (_props: BaseProps & Props): ComponentNode => {
|
||||
throw new Error(`@videojs/compiler: <${name}${part ? `.${part}` : ''}> can only be evaluated by the compiler.`);
|
||||
};
|
||||
|
||||
Object.assign(fn, { $$component: { name, part } });
|
||||
|
||||
return fn as Component<Props>;
|
||||
}
|
||||
|
||||
export function createComponent<
|
||||
M extends ComponentManifest<object, readonly string[], Partial<Record<string, object>>>,
|
||||
>(manifest: M): CreateComponentResult<M> {
|
||||
const parts = manifest.parts ?? [];
|
||||
|
||||
if (parts.length === 0) {
|
||||
return makePart(manifest.name, null) as CreateComponentResult<M>;
|
||||
}
|
||||
|
||||
const compound: Record<string, Component<never>> = {};
|
||||
|
||||
for (const part of parts) {
|
||||
compound[part] = makePart(manifest.name, part);
|
||||
}
|
||||
|
||||
return compound as CreateComponentResult<M>;
|
||||
}
|
||||
|
||||
function createNode(type: ComponentType, props: Record<string, unknown>, key?: string | number | null): ComponentNode {
|
||||
return {
|
||||
[VIDEOJS_NODE]: true,
|
||||
type,
|
||||
props,
|
||||
key: key ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function jsx(type: ComponentType, props: Record<string, unknown>, key?: string | number | null): ComponentNode {
|
||||
return createNode(type, props, key);
|
||||
}
|
||||
|
||||
export function jsxs(type: ComponentType, props: Record<string, unknown>, key?: string | number | null): ComponentNode {
|
||||
return createNode(type, props, key);
|
||||
}
|
||||
|
||||
export const Fragment: unique symbol = Symbol.for('@videojs/fragment') as never;
|
||||
|
||||
export namespace JSX {
|
||||
export type Element = ComponentNode;
|
||||
|
||||
export interface ElementChildrenAttribute {
|
||||
children: Record<string, never>;
|
||||
}
|
||||
|
||||
export interface IntrinsicAttributes {
|
||||
key?: string | number | undefined;
|
||||
}
|
||||
|
||||
export interface IntrinsicElements {
|
||||
div: BaseProps;
|
||||
span: BaseProps;
|
||||
}
|
||||
}
|
||||
@@ -43,23 +43,10 @@ export async function loadConfigFile(configPath: string): Promise<LoadedCompiler
|
||||
if (!config) {
|
||||
throw new Error(`Config file ${configPath} must export a default compiler config (use \`defineConfig\`).`);
|
||||
}
|
||||
return { config: resolveConfigPaths(config, configPath), configPath, configDir: dirname(configPath) };
|
||||
return { config, configPath, configDir: dirname(configPath) };
|
||||
}
|
||||
|
||||
export async function loadConfig(cwd: string, override: string | undefined): Promise<LoadedCompilerConfig | null> {
|
||||
const configPath = findConfig(cwd, override);
|
||||
return configPath ? loadConfigFile(configPath) : null;
|
||||
}
|
||||
|
||||
function resolveConfigPaths(config: CompilerConfig, configPath: string): CompilerConfig {
|
||||
if (!config.generate) return config;
|
||||
const base = dirname(configPath);
|
||||
const { output, components } = config.generate;
|
||||
return {
|
||||
...config,
|
||||
generate: {
|
||||
components,
|
||||
output: isAbsolute(output) ? output : resolve(base, output),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* React-target plugins for `@videojs/compiler`. Houses the framework-pattern
|
||||
* helpers that lower constrained-JSX skin idioms into React's render-prop
|
||||
* slot composition idiom. Re-exports `replace` and `wrap` for convenience so
|
||||
* a config can import everything from one subpath.
|
||||
* helpers that lower constrained JSX into React-friendly component shapes.
|
||||
* Re-exports `replace` and `wrap` for convenience so a config can import
|
||||
* everything from one subpath.
|
||||
*/
|
||||
|
||||
export { type ReplaceOptions, replace } from '../transforms/replace';
|
||||
|
||||
@@ -46,7 +46,7 @@ export interface UtilityCss {
|
||||
variants: readonly Variant[];
|
||||
/**
|
||||
* `@property` registrations Tailwind emitted for this utility. These supply
|
||||
* the typed defaults for `--tw-*` slots referenced (but not set) by the
|
||||
* the typed defaults for `--tw-*` variables referenced (but not set) by the
|
||||
* declarations — see `emitCss`'s `properties` option.
|
||||
*/
|
||||
properties?: readonly PropertyRule[];
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface DesignSystem {
|
||||
* Resolve a `@theme` variable (e.g. `--spacing`, `--color-white`) to its
|
||||
* value, or `undefined` if the theme doesn't define it. Used to emit a
|
||||
* self-contained theme block for the variables compiled rules reference.
|
||||
* Returns `undefined` for `@property`-registered slots like `--tw-*`.
|
||||
* Returns `undefined` for `@property`-registered variables like `--tw-*`.
|
||||
*/
|
||||
resolveThemeVar(name: string): string | undefined;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export interface EmitCssOptions {
|
||||
/**
|
||||
* Inline matching CSS custom properties into the values that reference
|
||||
* them, then drop the declarations themselves. Useful for stripping
|
||||
* Tailwind's internal `--tw-*` slots from the final output.
|
||||
* Tailwind's internal `--tw-*` registered variables from the final output.
|
||||
*
|
||||
* - `true` — inline `--tw-*` (regex `/^--tw-/`).
|
||||
* - `RegExp` — inline any `--name` whose name (excluding the leading
|
||||
@@ -81,7 +81,7 @@ export interface EmitCssOptions {
|
||||
* output references but doesn't itself declare, so the CSS resolves without a
|
||||
* separate Tailwind theme/preflight on the page. Typically
|
||||
* `design.resolveThemeVar`. Returns `undefined` to leave a variable alone
|
||||
* (e.g. `@property`-registered `--tw-*` slots).
|
||||
* (e.g. `@property`-registered `--tw-*` variables).
|
||||
*/
|
||||
resolveThemeVar?: (name: string) => string | undefined;
|
||||
/**
|
||||
@@ -90,7 +90,7 @@ export interface EmitCssOptions {
|
||||
*/
|
||||
themeSelector?: string;
|
||||
/**
|
||||
* How to handle Tailwind's `@property`-registered slots (e.g. `--tw-content`,
|
||||
* How to handle Tailwind's `@property`-registered variables (e.g. `--tw-content`,
|
||||
* `--tw-shadow`) that the compiled rules reference but never set locally.
|
||||
* Without this they resolve to nothing and break — e.g. `content:
|
||||
* var(--tw-content)` suppresses the `::after`/`::before` box.
|
||||
@@ -112,12 +112,12 @@ export interface PropertyDef {
|
||||
|
||||
export interface RegisteredPropertiesOptions {
|
||||
/**
|
||||
* - `'emit'` — emit `@property` rules for referenced slots, preserving
|
||||
* - `'emit'` — emit `@property` rules for referenced variables, preserving
|
||||
* Tailwind's typed defaults (relies on browser `@property` support).
|
||||
* - `'inline'` — substitute each slot's `initial-value` into the values that
|
||||
* - `'inline'` — substitute each variable's `initial-value` into the values that
|
||||
* reference it (and drop any `--tw-*` setter declarations), so the output
|
||||
* is fully self-contained. This is a superset of `inlineVars` for the
|
||||
* matched slots.
|
||||
* matched variables.
|
||||
*/
|
||||
mode: 'emit' | 'inline';
|
||||
/** Which property names to handle. Defaults to `inlineVars`'s matcher, else `/^--tw-/`. */
|
||||
@@ -126,7 +126,7 @@ export interface RegisteredPropertiesOptions {
|
||||
* Override or supply a property's definition. Receives the name and the
|
||||
* definition captured from Tailwind's output (if any); return a new
|
||||
* definition (merged in), or `undefined` to keep the captured one. Lets you
|
||||
* fix an initial-value or register a slot Tailwind didn't.
|
||||
* fix an initial-value or register a variable Tailwind didn't.
|
||||
*/
|
||||
resolve?: (name: string, captured: PropertyDef | undefined) => PropertyDef | undefined;
|
||||
}
|
||||
@@ -148,7 +148,7 @@ export async function emitCss(opts: EmitCssOptions): Promise<EmittedCss> {
|
||||
const hoist = opts.hoist === false ? undefined : opts.hoist;
|
||||
const inlineVars = normalizeInlineMatcher(opts.inlineVars);
|
||||
|
||||
// Registered `@property` (--tw-*) handling. In 'inline' mode the slots'
|
||||
// Registered `@property` (--tw-*) handling. In 'inline' mode the variables'
|
||||
// initial-values seed the inline pass as fallbacks (and the matcher widens to
|
||||
// cover them, even when `inlineVars` was off). In 'emit' mode they're left in
|
||||
// place to be emitted as `@property` rules.
|
||||
@@ -264,7 +264,7 @@ function collectDefinedVars(css: string): Set<string> {
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* Registered `@property` slots
|
||||
* Registered `@property` variables
|
||||
* ───────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/** Aggregate the `@property` defs captured across every rule (first wins). */
|
||||
@@ -296,7 +296,7 @@ function buildFallbackSetters(
|
||||
}
|
||||
|
||||
/**
|
||||
* Build `@property` rules for every matching slot the CSS references but
|
||||
* Build `@property` rules for every matching variable the CSS references but
|
||||
* doesn't itself declare. Descriptors default to `syntax: "*"` / `inherits:
|
||||
* false` when a resolved def omits them.
|
||||
*/
|
||||
@@ -591,7 +591,7 @@ function applyInline(
|
||||
|
||||
// Effective setters, narrowest last: registered `@property` initial-values
|
||||
// (lowest), then hoist root, then rule-local. Initial-values resolve
|
||||
// references to slots no rule ever sets (e.g. `content: var(--tw-content)`).
|
||||
// references to variables no rule ever sets (e.g. `content: var(--tw-content)`).
|
||||
const setters = new Map<string, string>(fallbackSetters);
|
||||
for (const [name, value] of rootSetters) setters.set(name, value);
|
||||
for (const [name, value] of localSetters) setters.set(name, value);
|
||||
|
||||
@@ -78,7 +78,7 @@ export interface TailwindOptions {
|
||||
* Inline matching CSS custom properties into their consumers, dropping
|
||||
* the matching declarations. Same shape as `EmitCssOptions['inlineVars']`:
|
||||
*
|
||||
* - `true` — inline `--tw-*` (Tailwind's internal slots).
|
||||
* - `true` — inline `--tw-*` (Tailwind's internal registered variables).
|
||||
* - `RegExp` — inline any `--name` matching.
|
||||
* - omitted — no inlining.
|
||||
*
|
||||
@@ -86,7 +86,7 @@ export interface TailwindOptions {
|
||||
*/
|
||||
inlineVars?: true | RegExp;
|
||||
/**
|
||||
* Handle Tailwind's `@property`-registered slots (`--tw-content`, etc.) that
|
||||
* Handle Tailwind's `@property`-registered variables (`--tw-content`, etc.) that
|
||||
* are referenced but never set, so the output isn't broken (e.g.
|
||||
* `content: var(--tw-content)`). See `RegisteredPropertiesOptions` — choose
|
||||
* `'emit'` (ship `@property` rules) or `'inline'` (bake initial-values in),
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('decompose — variants', () => {
|
||||
});
|
||||
|
||||
describe('decompose — @property registrations', () => {
|
||||
it('captures the @property rule Tailwind appends for a slot', () => {
|
||||
it('captures the @property rule Tailwind appends for a registered variable', () => {
|
||||
const r = decompose('before:content-["x"]', design);
|
||||
expect(r).not.toBeNull();
|
||||
const content = r!.properties?.find((p) => p.name === '--tw-content');
|
||||
|
||||
@@ -533,7 +533,7 @@ describe('emitCss — theme variables', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitCss — registered @property slots', () => {
|
||||
describe('emitCss — registered @property variables', () => {
|
||||
// The `after:absolute` pattern: an `::after` rule that references
|
||||
// `--tw-content` but never sets it, relying on Tailwind's @property default.
|
||||
const contentRule = (): CompiledRule => ({
|
||||
@@ -591,7 +591,7 @@ describe('emitCss — registered @property slots', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('honours the match filter (leaves non-matching slots alone)', async () => {
|
||||
it('honours the match filter (leaves non-matching variables alone)', async () => {
|
||||
const out = await emitCss({
|
||||
rules: [contentRule()],
|
||||
properties: { mode: 'inline', match: /^--brand-/ },
|
||||
@@ -600,7 +600,7 @@ describe('emitCss — registered @property slots', () => {
|
||||
expect(out.css).toMatch(/var\(--tw-content\)/);
|
||||
});
|
||||
|
||||
it('leaves slots untouched when no properties option is given (back-compat)', async () => {
|
||||
it('leaves variables untouched when no properties option is given (back-compat)', async () => {
|
||||
const out = await emitCss({ rules: [contentRule()] });
|
||||
if (out.kind !== 'merged') throw new Error('expected merged');
|
||||
expect(out.css).toMatch(/var\(--tw-content\)/);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { compile, type ReactTargetOptions, react } from '..';
|
||||
import { accessPath, compile, jsxExpression, type ReactTargetOptions, react, replaceJsxChild } from '..';
|
||||
import { parse } from '../ast';
|
||||
import { anyTag, byTag, hasChild } from '../matchers';
|
||||
import { addProp, childAsProp, replace, wrap } from '../react';
|
||||
@@ -53,14 +53,14 @@ describe('compile (transformImports — bare-string rule)', () => {
|
||||
|
||||
describe('compile (transformImports — function rule)', () => {
|
||||
it('rewrites per-identifier source and bucket-merges by resolved target', async () => {
|
||||
const source = `import { PlayButton, MuteButton } from '@videojs/core/components';\nconst _ = [PlayButton, MuteButton];`;
|
||||
const source = `import { Alpha, Beta } from '@fixture/components';\nconst _ = [Alpha, Beta];`;
|
||||
const { code } = await compileReact(source, {
|
||||
imports: {
|
||||
'@videojs/core/components': (name) => ({ source: `./ui/${name.toLowerCase()}`, name }),
|
||||
'@fixture/components': (name) => ({ source: `./ui/${name.toLowerCase()}`, name }),
|
||||
},
|
||||
});
|
||||
expect(code).toContain(`import { PlayButton } from "./ui/playbutton"`);
|
||||
expect(code).toContain(`import { MuteButton } from "./ui/mutebutton"`);
|
||||
expect(code).toContain(`import { Alpha } from "./ui/alpha"`);
|
||||
expect(code).toContain(`import { Beta } from "./ui/beta"`);
|
||||
});
|
||||
|
||||
it('renames identifiers when the rule returns a different `name`', async () => {
|
||||
@@ -139,6 +139,22 @@ describe('childAsProp', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceJsxChild', () => {
|
||||
it('replaces matched JSX children with expression helpers', async () => {
|
||||
const source = `function App({ values }){ return <Container><Token name="poster-image"/></Container>; }`;
|
||||
const { code } = await compileReact(source, {
|
||||
transforms: [
|
||||
replaceJsxChild({
|
||||
match: byTag('Token'),
|
||||
replace: (_node, factory) => jsxExpression(factory, accessPath(factory, 'values', 'poster-image')),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(collapse(code)).toContain(collapse(`<Container>{values["poster-image"]}</Container>`));
|
||||
});
|
||||
});
|
||||
|
||||
describe('addProp', () => {
|
||||
it('emits a JSX value by default and adds the import', async () => {
|
||||
const source = `function App(){ return <PlayButton/>; }`;
|
||||
|
||||
+3
-3
@@ -20,7 +20,7 @@ import {
|
||||
TimeSlider,
|
||||
Tooltip,
|
||||
VolumeSlider,
|
||||
} from '@videojs/core/components';
|
||||
} from '@fixture/components';
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@videojs/icons/components';
|
||||
} from '@fixture/icons/components';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { video as styles } from '../tailwind';
|
||||
|
||||
@@ -52,7 +52,7 @@ export interface VideoSkinProps {
|
||||
|
||||
export function VideoSkin({ className }: VideoSkinProps) {
|
||||
return (
|
||||
<Container data-skin="default-video" className={cn(styles.root, className)}>
|
||||
<Container data-skin="default-video" className={cn(styles.container, className)}>
|
||||
<Poster className={styles.poster} />
|
||||
|
||||
<BufferingIndicator className={styles.bufferingIndicator.root}>
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { generate } from '../generate';
|
||||
|
||||
const STUB = 'const defineComponent: any = () => (m: any) => m;';
|
||||
|
||||
function setup(): { dir: string; output: string; pattern: string } {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'videojs-compiler-'));
|
||||
mkdirSync(join(dir, 'play-button'));
|
||||
mkdirSync(join(dir, 'slider'));
|
||||
mkdirSync(join(dir, 'hotkey'));
|
||||
|
||||
writeFileSync(
|
||||
join(dir, 'play-button', 'play-button-data-attrs.ts'),
|
||||
`export const PlayButtonDataAttrs = {} as const;`
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, 'play-button', 'play-button-component.ts'),
|
||||
`import { PlayButtonDataAttrs } from './play-button-data-attrs';
|
||||
${STUB}
|
||||
export default defineComponent<{ disabled?: boolean }>()({
|
||||
name: 'PlayButton',
|
||||
dataAttrs: PlayButtonDataAttrs,
|
||||
});`
|
||||
);
|
||||
|
||||
writeFileSync(join(dir, 'slider', 'slider-parts.ts'), `export const SliderParts = ['Root', 'Track'] as const;`);
|
||||
writeFileSync(join(dir, 'slider', 'slider-data-attrs.ts'), `export const SliderDataAttrs = {} as const;`);
|
||||
writeFileSync(
|
||||
join(dir, 'slider', 'slider-component.ts'),
|
||||
`import { SliderDataAttrs } from './slider-data-attrs';
|
||||
import { SliderParts } from './slider-parts';
|
||||
${STUB}
|
||||
export default defineComponent<{ orientation?: 'horizontal' | 'vertical' }>()({
|
||||
name: 'Slider',
|
||||
parts: SliderParts,
|
||||
dataAttrs: SliderDataAttrs,
|
||||
});`
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
join(dir, 'hotkey', 'hotkey-component.ts'),
|
||||
`${STUB}
|
||||
export default defineComponent()({ name: 'Hotkey' });`
|
||||
);
|
||||
|
||||
return { dir, output: join(dir, 'out.ts'), pattern: join(dir, '*/*-component.ts') };
|
||||
}
|
||||
|
||||
function setupBulk(): { dir: string; output: string } {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'videojs-compiler-bulk-'));
|
||||
mkdirSync(join(dir, 'assets'));
|
||||
writeFileSync(join(dir, 'assets', 'play.svg'), '<svg/>');
|
||||
writeFileSync(join(dir, 'assets', 'pause.svg'), '<svg/>');
|
||||
return { dir, output: join(dir, 'out.ts') };
|
||||
}
|
||||
|
||||
describe('generate (manifest entries)', () => {
|
||||
it('imports each manifest as `<Name>Def` default-import', async () => {
|
||||
const { output, pattern } = setup();
|
||||
await generate({ generate: { components: [pattern], output } });
|
||||
const source = readFileSync(output, 'utf8');
|
||||
expect(source).toContain("import PlayButtonDef from './play-button/play-button-component';");
|
||||
expect(source).toContain("import SliderDef from './slider/slider-component';");
|
||||
expect(source).toContain("import HotkeyDef from './hotkey/hotkey-component';");
|
||||
});
|
||||
|
||||
it('emits createComponent(Def) for each component', async () => {
|
||||
const { output, pattern } = setup();
|
||||
await generate({ generate: { components: [pattern], output } });
|
||||
const source = readFileSync(output, 'utf8');
|
||||
expect(source).toContain('export const PlayButton = createComponent(PlayButtonDef);');
|
||||
expect(source).toContain('export const Slider = createComponent(SliderDef);');
|
||||
expect(source).toContain('export const Hotkey = createComponent(HotkeyDef);');
|
||||
});
|
||||
|
||||
it('emits COMPONENTS referencing each definition', async () => {
|
||||
const { output, pattern } = setup();
|
||||
await generate({ generate: { components: [pattern], output } });
|
||||
const source = readFileSync(output, 'utf8');
|
||||
expect(source).toContain('export const COMPONENTS = {');
|
||||
expect(source).toContain('export type Components = typeof COMPONENTS;');
|
||||
expect(source).toContain('PlayButton: PlayButtonDef,');
|
||||
expect(source).toContain('Slider: SliderDef,');
|
||||
expect(source).toContain('Hotkey: HotkeyDef,');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generate (bulk entries)', () => {
|
||||
it('inlines createComponent({ name }) for each matched file', async () => {
|
||||
const { dir, output } = setupBulk();
|
||||
await generate({
|
||||
generate: {
|
||||
components: [{ files: join(dir, 'assets/*.svg'), name: (f) => `${f[0]!.toUpperCase()}${f.slice(1)}Icon` }],
|
||||
output,
|
||||
},
|
||||
});
|
||||
const source = readFileSync(output, 'utf8');
|
||||
expect(source).toContain("export const PauseIcon = createComponent({ name: 'PauseIcon' });");
|
||||
expect(source).toContain("export const PlayIcon = createComponent({ name: 'PlayIcon' });");
|
||||
});
|
||||
|
||||
it('emits COMPONENTS with inline manifests for bulk entries', async () => {
|
||||
const { dir, output } = setupBulk();
|
||||
await generate({
|
||||
generate: {
|
||||
components: [{ files: join(dir, 'assets/*.svg'), name: (f) => `${f[0]!.toUpperCase()}${f.slice(1)}Icon` }],
|
||||
output,
|
||||
},
|
||||
});
|
||||
const source = readFileSync(output, 'utf8');
|
||||
expect(source).toContain("PlayIcon: { name: 'PlayIcon' },");
|
||||
expect(source).toContain("PauseIcon: { name: 'PauseIcon' },");
|
||||
});
|
||||
|
||||
it('strips the file extension before passing to name()', async () => {
|
||||
const { dir, output } = setupBulk();
|
||||
let received: string | null = null;
|
||||
await generate({
|
||||
generate: {
|
||||
components: [
|
||||
{
|
||||
files: join(dir, 'assets/*.svg'),
|
||||
name: (f) => {
|
||||
if (received === null) received = f;
|
||||
return `${f}Icon`;
|
||||
},
|
||||
},
|
||||
],
|
||||
output,
|
||||
},
|
||||
});
|
||||
expect(received).not.toContain('.svg');
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ const skinSource = resolve(__dirname, 'fixtures/video-skin.tsx');
|
||||
/**
|
||||
* End-to-end smoke test: feed a representative constrained-JSX video skin
|
||||
* (vendored under `fixtures/`) through `compile()` with the same shape
|
||||
* `@videojs/react`'s build hook uses, and sanity-check the output's structural
|
||||
* a React package build hook uses, and sanity-check the output's structural
|
||||
* shape. Snapshot-style assertions intentionally use `.toContain` over a full
|
||||
* snapshot to keep the test resilient to incidental whitespace differences
|
||||
* from the TS printer.
|
||||
@@ -22,11 +22,11 @@ describe('integration: default/video skin → React', () => {
|
||||
let code = '';
|
||||
|
||||
const imports: Record<string, ImportRule> = {
|
||||
'@videojs/core/components': (name) => ({
|
||||
'@fixture/components': (name) => ({
|
||||
source: `./src/ui/${name.replace(/^[A-Z]/, (m) => m.toLowerCase()).replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}`,
|
||||
name,
|
||||
}),
|
||||
'@videojs/icons/components': '@videojs/icons/react',
|
||||
'@fixture/icons/components': '@fixture/icons/react',
|
||||
'../tailwind': '@videojs/skins/default/tailwind',
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('integration: default/video skin → React', () => {
|
||||
code = result.code;
|
||||
});
|
||||
|
||||
it('rewrites @videojs/core/components imports to per-identifier UI sources', () => {
|
||||
it('rewrites component imports to per-identifier UI sources', () => {
|
||||
expect(code).toMatch(/import \{ PlayButton \} from "\.\/src\/ui\/play-button"/);
|
||||
// MuteButton lives under the volume Popover.Root subtree, which is replaced
|
||||
// wholesale by VolumePopover — its import is correctly dropped by the
|
||||
@@ -59,9 +59,9 @@ describe('integration: default/video skin → React', () => {
|
||||
expect(code).not.toMatch(/import \{ MuteButton \}/);
|
||||
});
|
||||
|
||||
it('rewrites @videojs/icons/components to @videojs/icons/react', () => {
|
||||
expect(code).toContain('@videojs/icons/react');
|
||||
expect(code).not.toContain('@videojs/icons/components');
|
||||
it('rewrites icon component imports', () => {
|
||||
expect(code).toContain('@fixture/icons/react');
|
||||
expect(code).not.toContain('@fixture/icons/components');
|
||||
});
|
||||
|
||||
it('substitutes the volume Popover.Root with VolumePopover', () => {
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/** @jsxImportSource @videojs/compiler */
|
||||
|
||||
import { PlayButton, Slider, Time } from '@videojs/core/components';
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
describe('constrained JSX', () => {
|
||||
it('accepts a single component', () => {
|
||||
void (<PlayButton className="x" />);
|
||||
});
|
||||
|
||||
it('rejects invalid props on a single component', () => {
|
||||
// @ts-expect-error - className must be a string
|
||||
void (<PlayButton className={5} />);
|
||||
});
|
||||
|
||||
it('accepts compound parts inside their root', () => {
|
||||
void (
|
||||
<Slider.Root orientation="vertical" thumbAlignment="edge">
|
||||
<Slider.Track>
|
||||
<Slider.Fill />
|
||||
</Slider.Track>
|
||||
<Slider.Thumb />
|
||||
</Slider.Root>
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects invalid compound root props', () => {
|
||||
// @ts-expect-error - `bogus` is not a valid orientation
|
||||
void (<Slider.Root orientation="bogus" />);
|
||||
});
|
||||
|
||||
it('rejects invalid Time.Value props', () => {
|
||||
void (<Time.Value type="current" className="t" />);
|
||||
// @ts-expect-error - `bogus` not in Time type union
|
||||
void (<Time.Value type="bogus" />);
|
||||
});
|
||||
|
||||
it('accepts div and span as layout intrinsics', () => {
|
||||
void (
|
||||
<div className="row">
|
||||
<span className="label">hello</span>
|
||||
</div>
|
||||
);
|
||||
// @ts-expect-error - arbitrary HTML attributes (id) are not allowed on layout intrinsics
|
||||
void (<div id="foo" />);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@videojs/compiler",
|
||||
"noEmit": true,
|
||||
"isolatedDeclarations": false,
|
||||
"composite": false,
|
||||
"incremental": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false
|
||||
},
|
||||
"include": ["**/*.test-d.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { isString } from '@videojs/utils/predicate';
|
||||
import ts from 'typescript';
|
||||
import type { JsxElementLike, Matcher } from '../matchers';
|
||||
|
||||
const IDENTIFIER_NAME_RE = /^[$A-Z_a-z][$\w]*$/;
|
||||
|
||||
export type JsxChildReplacement = ts.JsxChild | readonly ts.JsxChild[];
|
||||
|
||||
export interface ReplaceJsxChildOptions {
|
||||
match: Matcher;
|
||||
replace: (node: JsxElementLike, factory: ts.NodeFactory) => JsxChildReplacement | undefined;
|
||||
}
|
||||
|
||||
export function replaceJsxChild(options: ReplaceJsxChildOptions): ts.TransformerFactory<ts.SourceFile> {
|
||||
return (context) => {
|
||||
const visit: ts.Visitor = (node) => {
|
||||
if (isJsxElementLike(node) && options.match(node)) {
|
||||
const replacement = options.replace(node, context.factory);
|
||||
return replacement ?? node;
|
||||
}
|
||||
|
||||
return ts.visitEachChild(node, visit, context);
|
||||
};
|
||||
|
||||
return (sourceFile) => ts.visitEachChild(sourceFile, visit, context);
|
||||
};
|
||||
}
|
||||
|
||||
export function jsxExpression(factory: ts.NodeFactory, expression: ts.Expression): ts.JsxExpression {
|
||||
return factory.createJsxExpression(undefined, expression);
|
||||
}
|
||||
|
||||
export function accessPath(
|
||||
factory: ts.NodeFactory,
|
||||
root: string | ts.Expression,
|
||||
...path: readonly string[]
|
||||
): ts.Expression {
|
||||
let expression = isString(root) ? factory.createIdentifier(root) : root;
|
||||
|
||||
for (const property of path) {
|
||||
expression = propertyAccess(factory, expression, property);
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
export function propertyAccess(factory: ts.NodeFactory, expression: ts.Expression, property: string): ts.Expression {
|
||||
if (IDENTIFIER_NAME_RE.test(property)) {
|
||||
return factory.createPropertyAccessExpression(expression, property);
|
||||
}
|
||||
|
||||
return factory.createElementAccessExpression(expression, factory.createStringLiteral(property));
|
||||
}
|
||||
|
||||
export function readStringAttribute(attributes: ts.JsxAttributes, name: string): string | null | undefined {
|
||||
const attr = attributes.properties.find(
|
||||
(property) => ts.isJsxAttribute(property) && ts.isIdentifier(property.name) && property.name.text === name
|
||||
);
|
||||
|
||||
if (!attr || !ts.isJsxAttribute(attr)) return undefined;
|
||||
|
||||
const init = attr.initializer;
|
||||
if (!init) return '';
|
||||
if (ts.isStringLiteral(init)) return init.text;
|
||||
if (ts.isJsxExpression(init) && init.expression) {
|
||||
if (ts.isStringLiteral(init.expression) || ts.isNoSubstitutionTemplateLiteral(init.expression)) {
|
||||
return init.expression.text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isJsxElementLike(node: ts.Node): node is JsxElementLike {
|
||||
return ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@videojs/compiler"
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ export default defineConfig({
|
||||
entry: {
|
||||
index: './src/index.ts',
|
||||
cli: './src/cli.ts',
|
||||
'jsx-runtime': './src/jsx-runtime.ts',
|
||||
'jsx-dev-runtime': './src/jsx-dev-runtime.ts',
|
||||
'plugins/vite': './src/plugins/vite.ts',
|
||||
'ast/index': './src/ast/index.ts',
|
||||
'matchers/index': './src/matchers/index.ts',
|
||||
|
||||
Reference in New Issue
Block a user