Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
!*.config.*js
websites/**/dist
websites/**/tsconfig.json
syncScripts/**/dist
syncScripts/**/tsconfig.json
node_modules

# MacOS-generated files
Expand Down
140 changes: 140 additions & 0 deletions @types/premid/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,4 +799,144 @@
UpdateData: []
}

// Sync Script types

interface SyncScriptPlaybackReport {
playing: boolean
currentTime: number
duration: number
playbackRate?: number
}

interface SyncScriptSyncCommand {
action: 'play' | 'pause' | 'seek' | 'rate'
currentTime?: number
playbackRate?: number
capturedAt?: number
}

interface SyncScriptPageInfo {
title?: string
subtitle?: string
thumbnail?: string
}

interface SyncScriptTakeControlOptions {
onPlay: (timeSeconds: number) => void
onPause: (timeSeconds: number) => void
onSeek: (timeSeconds: number) => void
onRate?: (playbackRate: number) => void
driftThreshold?: number
pauseAfterSeek?: boolean | number
}

interface AutoVideoHandle {
detach(): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
onSyncCommand(handler: (cmd: SyncScriptSyncCommand) => void): () => void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
reportAd(isInAd: boolean): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
reportBuffering(isBuffering: boolean): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

interface ManualVideoHandle {
reportPlayback(status: SyncScriptPlaybackReport): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
onSyncCommand(handler: (cmd: SyncScriptSyncCommand) => void): () => void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
reportAd(isInAd: boolean): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
reportBuffering(isBuffering: boolean): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
release(): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

interface SyncScriptVideoAPI {
attach(element: HTMLMediaElement | string): AutoVideoHandle

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
takeControl(options: SyncScriptTakeControlOptions): ManualVideoHandle

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
reportAd(isInAd: boolean): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
reportBuffering(isBuffering: boolean): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

interface MessagingNamespace {
send(key: string, data: unknown): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
request(key: string, data: unknown): Promise<unknown>

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
onMessage(key: string, handler: (data: unknown) => void): () => void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
onRequest(key: string, handler: (data: unknown) => unknown | Promise<unknown>): () => void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

interface IframeMessaging {
send(frameId: number, key: string, data: unknown): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
request(frameId: number, key: string, data: unknown): Promise<unknown>

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
onMessage(key: string, handler: (data: unknown, frameId: number) => void): () => void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
onRequest(key: string, handler: (data: unknown, frameId: number) => unknown | Promise<unknown>): () => void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

interface PartyState {
readonly role: 'controller' | 'follower'
readonly paused: boolean
readonly active: boolean
readonly isHost: boolean
onRoleChange(handler: (role: 'controller' | 'follower') => void): () => void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
onPauseChange(handler: (paused: boolean) => void): () => void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
onActiveChange(handler: (active: boolean) => void): () => void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

interface SyncScriptContext {
features(features: { video?: boolean, cursor?: boolean, scroll?: boolean }): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.

video: SyncScriptVideoAPI

setPageInfo(info: SyncScriptPageInfo): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.

sendCustomData(key: string, data: unknown): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
onCustomData(key: string, handler: (data: unknown) => void): () => void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.

iframe: IframeMessaging
mainworld: MessagingNamespace

party: PartyState
}

interface SyncScriptDefinition {
setup(ctx: SyncScriptContext): (() => void) | void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
onNavigate?(ctx: SyncScriptContext, url: string): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

interface SyncScriptHandle {
destroy(): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

interface SyncIframeVideoAPI {
attach(element: HTMLMediaElement | string): AutoVideoHandle

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
onSyncCommand(handler: (cmd: SyncScriptSyncCommand) => void): () => void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

interface SyncIframeContext {
video: SyncIframeVideoAPI

sendCustomData(key: string, data: unknown): void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
onCustomData(key: string, handler: (data: unknown) => void): () => void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.

content: MessagingNamespace
}

interface SyncIframeScriptDefinition {
setup(ctx: SyncIframeContext): (() => void) | void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

interface SyncMainworldContext {
content: MessagingNamespace
}

interface SyncMainworldScriptDefinition {
setup(ctx: SyncMainworldContext): (() => void) | void

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

const SyncScript: {
register(definition: SyncScriptDefinition): SyncScriptHandle

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

const SyncIframeScript: {
register(definition: SyncIframeScriptDefinition): SyncScriptHandle

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

const SyncMainworldScript: {
register(definition: SyncMainworldScriptDefinition): SyncScriptHandle

Check failure

Code scanning / ESLint

Enforce using a particular method signature syntax Error

Shorthand method signature is forbidden. Use a function property instead.
}

}
214 changes: 214 additions & 0 deletions cli/src/classes/SyncScriptCompiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { existsSync } from 'node:fs'
import { cp, rm } from 'node:fs/promises'
import { basename, dirname, resolve } from 'node:path'
import chalk from 'chalk'
import { watch } from 'chokidar'
import { build } from 'esbuild'
import ora from 'ora'
import ts from 'typescript'
import { error, exit, info, prefix } from '../util/log.js'
import { WebSocketServer } from './WebSocketServer.js'

export interface SyncScriptMetadata {
service: string
regExp: string
iframeRegExp?: string
}

export class SyncScriptCompiler {
ws: WebSocketServer | undefined

constructor(
public readonly cwd: string,
public readonly metadata: SyncScriptMetadata,
) {}

async compile({ kill }: { kill: boolean }): Promise<boolean> {
const success = await this.typecheck(kill)
if (!success)
return false

const spinner = ora(prefix + chalk.greenBright(` Compiling ${this.metadata.service}...`))
spinner.start()

const hasIframe = existsSync(resolve(this.cwd, 'iframe.ts'))
const hasMainworld = existsSync(resolve(this.cwd, 'mainworld.ts'))

if (existsSync(resolve(this.cwd, 'dist')))
await rm(resolve(this.cwd, 'dist'), { recursive: true })

await build({
entryPoints: [
resolve(this.cwd, 'content.ts'),
...(hasIframe ? [resolve(this.cwd, 'iframe.ts')] : []),
...(hasMainworld ? [resolve(this.cwd, 'mainworld.ts')] : []),
],
outdir: resolve(this.cwd, 'dist'),
bundle: true,
minify: true,
sourcemap: 'inline',
tsconfig: resolve(this.cwd, 'tsconfig.json'),
})

await cp(resolve(this.cwd, 'metadata.json'), resolve(this.cwd, 'dist', 'metadata.json'))

spinner.succeed(prefix + chalk.greenBright(` Compiled ${this.metadata.service}!`))
return true
}

async watch() {
this.ws = new WebSocketServer(this.cwd, 'localSyncScript')

watch(this.cwd, {
depth: 0,
ignoreInitial: true,
ignored: ['**/dist/**'],
persistent: true,
}).on('all', async (event, path) => {
if (['add', 'unlink'].includes(event) && (basename(path) === 'iframe.ts' || basename(path) === 'mainworld.ts')) {
return this.restartTsWatch()
}

if (event === 'change' && basename(path) === 'metadata.json') {
return this.compileAndSend()
}
})

this.startTsWatch()
}

private program: ts.WatchOfFilesAndCompilerOptions<ts.SemanticDiagnosticsBuilderProgram> | undefined

private startTsWatch() {
const hasIframe = existsSync(resolve(this.cwd, 'iframe.ts'))
const hasMainworld = existsSync(resolve(this.cwd, 'mainworld.ts'))
const tsconfigPath = resolve(this.cwd, 'tsconfig.json')

const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile)
if (configFile.error) {
error('Failed to read tsconfig.json:')
exit(ts.flattenDiagnosticMessageText(configFile.error.messageText, '\n'))
}

const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, this.cwd)
if (parsedConfig.errors.length) {
error('Failed to parse tsconfig.json:')
parsedConfig.errors.forEach((diagnostic) => {
error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'))
})
process.exit(1)

Check failure

Code scanning / ESLint

enforce either `process` or `require("process")` Error

Unexpected use of the global variable 'process'. Use 'require("process")' instead.
}

const host = ts.createWatchCompilerHost(
[resolve(this.cwd, 'content.ts'), ...(hasIframe ? [resolve(this.cwd, 'iframe.ts')] : []), ...(hasMainworld ? [resolve(this.cwd, 'mainworld.ts')] : [])],
{
...parsedConfig.options,
noEmit: true,
},
ts.sys,
ts.createSemanticDiagnosticsBuilderProgram,
(diagnostic) => {
if (!diagnostic.file) {
return error(ts.formatDiagnostic(diagnostic, {
getCanonicalFileName: fileName => fileName,
getCurrentDirectory: () => this.cwd,
getNewLine: () => '\n',
}))
}
error(chalk.white(`${chalk.cyan(
`${basename(dirname(diagnostic.file.fileName))}/${basename(diagnostic.file.fileName)}`,
)}`
+ `:${
chalk.yellowBright(diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!).line + 1)
}:${
chalk.yellowBright(diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!).character + 1)
} - ${
chalk.redBright('Error ')
}${chalk.gray(`TS${diagnostic.code}:`)
} ${
ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`))
},
(diagnostic) => {
info(diagnostic.messageText as string)

if (diagnostic.code === 6194 && (diagnostic.messageText as string).startsWith('Found 0 errors.')) {
this.compileAndSend()
}
},
)

this.program = ts.createWatchProgram(host)
}

private stopTsWatch() {
this.program?.updateRootFileNames([])
this.program?.close()
this.program = undefined
}

private restartTsWatch() {
this.stopTsWatch()
this.startTsWatch()
}

private async compileAndSend() {
await this.compile({ kill: false })
await this.ws?.send()
}

private async typecheck(killOnError: boolean): Promise<boolean> {
const spinner = ora(
prefix + chalk.yellow(` Type checking ${this.metadata.service}...`),
).start()

const hasIframe = existsSync(resolve(this.cwd, 'iframe.ts'))
const hasMainworld = existsSync(resolve(this.cwd, 'mainworld.ts'))
const tsconfigPath = resolve(this.cwd, 'tsconfig.json')

const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile)
if (configFile.error) {
spinner.fail(prefix + chalk.red(' Failed to read tsconfig.json:'))
exit(ts.flattenDiagnosticMessageText(configFile.error.messageText, '\n'))
}

const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, this.cwd)
if (parsedConfig.errors.length) {
spinner.fail(prefix + chalk.red(' Failed to parse tsconfig.json:'))
parsedConfig.errors.forEach((diagnostic) => {
error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'))
})
if (killOnError)
process.exit(1)

Check failure

Code scanning / ESLint

enforce either `process` or `require("process")` Error

Unexpected use of the global variable 'process'. Use 'require("process")' instead.
return false
}

const program = ts.createProgram({
rootNames: [resolve(this.cwd, 'content.ts'), ...(hasIframe ? [resolve(this.cwd, 'iframe.ts')] : []), ...(hasMainworld ? [resolve(this.cwd, 'mainworld.ts')] : [])],
options: { ...parsedConfig.options, noEmit: true },
})

const allDiagnostics = ts
.getPreEmitDiagnostics(program)
.concat(program.emit().diagnostics)

if (allDiagnostics.length > 0) {
spinner.fail(prefix + chalk.red(' Type checking failed:'))
allDiagnostics.forEach((diagnostic) => {
if (diagnostic.file) {
const { line, character } = ts.getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start!)
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')
error(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`)
}
else {
error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'))
}
})
if (killOnError)
process.exit(1)

Check failure

Code scanning / ESLint

enforce either `process` or `require("process")` Error

Unexpected use of the global variable 'process'. Use 'require("process")' instead.
return false
}

spinner.succeed(prefix + chalk.greenBright(` Type checking ${this.metadata.service} passed!`))
return true
}
}
Loading
Loading