build: added jsr building

This commit is contained in:
alina 🌸 2024-08-25 23:22:20 +03:00
parent 6166e27808
commit 8f0a4ec4aa
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
10 changed files with 357 additions and 114 deletions

View file

@ -110,22 +110,22 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REGISTRY: 'https://npm.tei.su' REGISTRY: 'https://npm.tei.su'
run: cd e2e/node && ./cli.sh ci-publish run: cd e2e/node && ./cli.sh ci-publish
# e2e-deno: e2e-deno:
# runs-on: ubuntu-latest runs-on: ubuntu-latest
# needs: [lint, test-node, test-web, test-bun, test-deno] needs: [lint, test-node, test-web, test-bun, test-deno]
# permissions: permissions:
# contents: read contents: read
# actions: write actions: write
# steps: steps:
# - uses: actions/checkout@v4 - uses: actions/checkout@v4
# - name: Run end-to-end tests under Deno - name: Run end-to-end tests under Deno
# env: env:
# API_ID: ${{ secrets.TELEGRAM_API_ID }} API_ID: ${{ secrets.TELEGRAM_API_ID }}
# API_HASH: ${{ secrets.TELEGRAM_API_HASH }} API_HASH: ${{ secrets.TELEGRAM_API_HASH }}
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# uses: nick-fields/retry@v2 uses: nick-fields/retry@v2
# # thanks docker networking very cool # thanks docker networking very cool
# with: with:
# max_attempts: 3 max_attempts: 3
# timeout_minutes: 30 timeout_minutes: 30
# command: cd e2e/deno && ./cli.sh ci command: cd e2e/deno && ./cli.sh ci

View file

@ -1,5 +1,7 @@
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { resolve } from 'node:path'
import * as fs from 'node:fs'
const KNOWN_DECORATORS = ['memoizeGetters', 'makeInspectable'] const KNOWN_DECORATORS = ['memoizeGetters', 'makeInspectable']
@ -68,5 +70,13 @@ export default () => {
}, },
}, },
], ],
finalJsr({ outDir }) {
const networkMgrFile = resolve(outDir, 'network/network-manager.ts')
const code = fs.readFileSync(networkMgrFile, 'utf8')
const require = createRequire(import.meta.url)
const version = require(fileURLToPath(new URL('./package.json', import.meta.url))).version
fs.writeFileSync(networkMgrFile, code.replace('%VERSION%', version))
},
} }
} }

View file

@ -1,7 +1,7 @@
import Long from 'long' import Long from 'long'
import type { mtp } from '@mtcute/tl' import type { mtp } from '@mtcute/tl'
import { timers } from '../utils' import { timers } from '../utils/index.js'
export class ServerSaltManager { export class ServerSaltManager {
private _futureSalts: mtp.RawMt_future_salt[] = [] private _futureSalts: mtp.RawMt_future_salt[] = []

View file

@ -4,48 +4,58 @@
// to the globals being typed incorrectly. // to the globals being typed incorrectly.
// instead, we can treat the timers as opaque objects, and expose // instead, we can treat the timers as opaque objects, and expose
// them through the `timers` esm namespace. // them through the `timers` esm namespace.
// this has zero runtime cost (as everything is stripped at compile time), // this has near-zero runtime cost, but makes everything type-safe
// but makes everything type-safe
// //
// the `import.meta.env.MODE === 'test'` is a workaround for vitest // NB: we are using wrapper functions instead of...
// not being able to mock timers because it mocks the globals // - directly exposing the globals because the standard doesn't allow that
// todo: we should probably do this as a vite plugin instead // - .bind()-ing because it makes it harder to mock the timer globals
export interface Timer { readonly __type: 'Timer' } export interface Timer { readonly __type: 'Timer' }
export interface Interval { readonly __type: 'Interval' } export interface Interval { readonly __type: 'Interval' }
export interface Immediate { readonly __type: 'Immediate' } export interface Immediate { readonly __type: 'Immediate' }
const setTimeoutWrap = ( const setTimeoutWrap = (
import.meta.env?.MODE === 'test' ? (...args: Parameters<typeof setTimeout>) => setTimeout(...args) : setTimeout (...args: Parameters<typeof setTimeout>) => setTimeout(...args)
) as unknown as <T extends (...args: any[]) => any>( ) as unknown as <T extends (...args: any[]) => any>(
fn: T, ms: number, ...args: Parameters<T> fn: T, ms: number, ...args: Parameters<T>
) => Timer ) => Timer
const setIntervalWrap = ( const setIntervalWrap = (
import.meta.env?.MODE === 'test' ? (...args: Parameters<typeof setInterval>) => setInterval(...args) : setInterval (...args: Parameters<typeof setInterval>) => setInterval(...args)
) as unknown as <T extends (...args: any[]) => any>( ) as unknown as <T extends (...args: any[]) => any>(
fn: T, ms: number, ...args: Parameters<T> fn: T, ms: number, ...args: Parameters<T>
) => Interval ) => Interval
const setImmediateWrap = (
typeof setImmediate !== 'undefined' ? setImmediate : setTimeout let setImmediateWrap: any
) as unknown as ( if (typeof setImmediate !== 'undefined') {
fn: () => void setImmediateWrap = (...args: Parameters<typeof setImmediate>) => setImmediate(...args)
} else {
// eslint-disable-next-line
setImmediateWrap = (fn: (...args: any[]) => void, ...args: any[]) => setTimeout(fn, 0, ...args)
}
const setImmediateWrapExported = setImmediateWrap as <T extends (...args: any[]) => any>(
fn: T, ...args: Parameters<T>
) => Immediate ) => Immediate
const clearTimeoutWrap = ( const clearTimeoutWrap = (
import.meta.env?.MODE === 'test' ? (...args: Parameters<typeof clearTimeout>) => clearTimeout(...args) : clearTimeout (...args: Parameters<typeof clearTimeout>) => clearTimeout(...args)
) as unknown as (timer?: Timer) => void ) as unknown as (timer?: Timer) => void
const clearIntervalWrap = ( const clearIntervalWrap = (
import.meta.env?.MODE === 'test' ? (...args: Parameters<typeof clearInterval>) => clearInterval(...args) : clearInterval (...args: Parameters<typeof clearInterval>) => clearInterval(...args)
) as unknown as (timer?: Interval) => void ) as unknown as (timer?: Interval) => void
const clearImmediateWrap = (
typeof clearImmediate !== 'undefined' ? clearImmediate : clearTimeout let clearImmediateWrap: any
) as unknown as (timer?: Immediate) => void if (typeof clearImmediate !== 'undefined') {
clearImmediateWrap = (...args: Parameters<typeof clearImmediate>) => clearImmediate(...args)
} else {
clearImmediateWrap = (timer: number) => clearTimeout(timer)
}
const clearImmediateWrapExported = clearImmediateWrap as (timer?: Immediate) => void
export { export {
setTimeoutWrap as setTimeout, setTimeoutWrap as setTimeout,
setIntervalWrap as setInterval, setIntervalWrap as setInterval,
setImmediateWrap as setImmediate, setImmediateWrapExported as setImmediate,
clearTimeoutWrap as clearTimeout, clearTimeoutWrap as clearTimeout,
clearIntervalWrap as clearInterval, clearIntervalWrap as clearInterval,
clearImmediateWrap as clearImmediate, clearImmediateWrapExported as clearImmediate,
} }

View file

@ -1,12 +0,0 @@
module.exports = ({ outDir, fs, jsr }) => ({
buildCjs: false,
final() {
if (jsr) {
// jsr doesn't support symlinks, so we need to copy the files manually
const real = fs.realpathSync(`${outDir}/common-internals-web`)
fs.unlinkSync(`${outDir}/common-internals-web`)
// console.log(real)
fs.cpSync(real, `${outDir}/common-internals-web`, { recursive: true })
}
},
})

View file

@ -0,0 +1,10 @@
import * as fs from 'node:fs'
export default () => ({
finalJsr({ outDir }) {
// jsr doesn't support symlinks, so we need to copy the files manually
const real = fs.realpathSync(`${outDir}/common-internals-web`)
fs.unlinkSync(`${outDir}/common-internals-web`)
fs.cpSync(real, `${outDir}/common-internals-web`, { recursive: true })
},
})

View file

@ -2,7 +2,6 @@ import { resolve } from 'node:path'
import * as fs from 'node:fs' import * as fs from 'node:fs'
export default () => ({ export default () => ({
// esmOnlyDirectives: true,
finalPackageJson(pkg) { finalPackageJson(pkg) {
pkg.exports['./mtcute.wasm'] = './mtcute.wasm' pkg.exports['./mtcute.wasm'] = './mtcute.wasm'
}, },

View file

@ -51,7 +51,8 @@ export function initSync(module: SyncInitInput): void {
module = new WebAssembly.Instance(module) module = new WebAssembly.Instance(module)
} }
wasm = module.exports as unknown as MtcuteWasmModule // eslint-disable-next-line
wasm = (module as unknown as WebAssembly.Instance).exports as unknown as MtcuteWasmModule
initCommon() initCommon()
} }

View file

@ -0,0 +1,220 @@
import { fileURLToPath } from 'node:url'
import * as fs from 'node:fs'
import * as cp from 'node:child_process'
import { resolve } from 'node:path'
import * as glob from 'glob'
import ts from 'typescript'
import { processPackageJson } from '../.config/vite-utils/package-json.js'
export function packageJsonToDeno({ packageJson, packageJsonOrig }) {
// https://jsr.io/docs/package-configuration
const importMap = {}
const exports = {}
if (packageJson.dependencies) {
for (const [name, version] of Object.entries(packageJson.dependencies)) {
if (name.startsWith('@mtcute/')) {
importMap[name] = `jsr:${name}@${version}`
} else if (version.startsWith('npm:@jsr/')) {
const jsrName = version.slice(9).split('@')[0].replace('__', '/')
const jsrVersion = version.slice(9).split('@')[1]
importMap[name] = `jsr:@${jsrName}@${jsrVersion}`
} else {
importMap[name] = `npm:${name}@${version}`
}
}
}
if (packageJsonOrig.exports) {
let tmpExports
if (typeof packageJsonOrig.exports === 'string') {
tmpExports = { '.': packageJsonOrig.exports }
} else if (typeof packageJsonOrig.exports !== 'object') {
throw new TypeError('package.json exports must be an object')
} else {
tmpExports = packageJsonOrig.exports
}
for (const [name, value] of Object.entries(tmpExports)) {
if (typeof value !== 'string') {
throw new TypeError(`package.json exports value must be a string: ${name}`)
}
if (value.endsWith('.wasm')) continue
exports[name] = value
.replace(/^\.\/src\//, './')
.replace(/\.js$/, '.ts')
}
}
return {
name: packageJson.name,
version: packageJson.version,
exports,
exclude: ['**/*.test.ts', '**/*.test-utils.ts', '**/__fixtures__/**'],
imports: importMap,
publish: {
exclude: ['!../dist'], // lol
},
...packageJson.denoJson,
}
}
export async function runJsrBuildSync(packageName) {
const packageDir = fileURLToPath(new URL(`../packages/${packageName}`, import.meta.url))
const outDir = fileURLToPath(new URL(`../packages/${packageName}/dist/jsr`, import.meta.url))
fs.rmSync(outDir, { recursive: true, force: true })
fs.mkdirSync(outDir, { recursive: true })
console.log('[i] Copying sources...')
fs.cpSync(resolve(packageDir, 'src'), outDir, { recursive: true })
const printer = ts.createPrinter()
for (const f of glob.sync(resolve(outDir, '**/*.ts'))) {
let fileContent = fs.readFileSync(f, 'utf8')
let changed = false
// replace .js imports with .ts
const file = ts.createSourceFile(f, fileContent, ts.ScriptTarget.ESNext, true)
let changedTs = false
for (const imp of file.statements) {
if (imp.kind !== ts.SyntaxKind.ImportDeclaration && imp.kind !== ts.SyntaxKind.ExportDeclaration) {
continue
}
if (imp.kind === ts.SyntaxKind.ExportDeclaration && !imp.moduleSpecifier) {
continue
}
const mod = imp.moduleSpecifier.text
if (mod[0] === '.' && mod.endsWith('.js')) {
changedTs = true
imp.moduleSpecifier = {
kind: ts.SyntaxKind.StringLiteral,
text: `${mod.slice(0, -3)}.ts`,
}
}
}
if (changedTs) {
fileContent = printer.printFile(file)
changed = true
}
// add shims for node-specific APIs and replace NodeJS.* types
// pretty fragile, but it works for now
const typesToReplace = {
'NodeJS\\.Timeout': 'number',
'NodeJS\\.Immediate': 'number',
}
const nodeSpecificApis = {
setImmediate: '(cb: (...args: any[]) => void, ...args: any[]) => number',
clearImmediate: '(id: number) => void',
Buffer:
'{ '
+ 'concat: (...args: any[]) => Uint8Array, '
+ 'from: (data: any, encoding?: string) => { toString(encoding?: string): string }, '
+ ' }',
SharedWorker: ['type', 'never'],
WorkerGlobalScope:
'{ '
+ ' new (): typeof WorkerGlobalScope, '
+ ' postMessage: (message: any, transfer?: Transferable[]) => void, '
+ ' addEventListener: (type: "message", listener: (ev: MessageEvent) => void) => void, '
+ ' }',
process: '{ ' + 'hrtime: { bigint: () => bigint }, ' + '}',
}
for (const [name, decl_] of Object.entries(nodeSpecificApis)) {
if (fileContent.includes(name)) {
if (name === 'Buffer' && fileContent.includes('node:buffer')) continue
changed = true
const isType = Array.isArray(decl_) && decl_[0] === 'type'
const decl = isType ? decl_[1] : decl_
if (isType) {
fileContent = `declare type ${name} = ${decl};\n${fileContent}`
} else {
fileContent = `declare const ${name}: ${decl};\n${fileContent}`
}
}
}
for (const [oldType, newType] of Object.entries(typesToReplace)) {
if (fileContent.match(oldType)) {
changed = true
fileContent = fileContent.replace(new RegExp(oldType, 'g'), newType)
}
}
if (changed) {
fs.writeFileSync(f, fileContent)
}
}
const { packageJson, packageJsonOrig } = processPackageJson(packageDir)
const denoJson = packageJsonToDeno({ packageJson, packageJsonOrig })
fs.writeFileSync(resolve(outDir, 'deno.json'), JSON.stringify(denoJson, null, 2))
fs.cpSync(new URL('../LICENSE', import.meta.url), resolve(outDir, 'LICENSE'), { recursive: true })
if (process.env.E2E) {
// populate dependencies, if any
const depsToPopulate = []
for (const dep of Object.values(denoJson.imports)) {
if (!dep.startsWith('jsr:')) continue
if (dep.startsWith('jsr:@mtcute/')) continue
depsToPopulate.push(dep.slice(4))
}
if (depsToPopulate.length) {
console.log('[i] Populating %d dependencies...', depsToPopulate.length)
cp.spawnSync(
'pnpm',
[
'exec',
'slow-types-compiler',
'populate',
'--downstream',
process.env.JSR_URL,
'--token',
process.env.JSR_TOKEN,
'--unstable-create-via-api',
...depsToPopulate,
],
{
stdio: 'inherit',
},
)
}
}
let customConfig
try {
customConfig = await (await import(resolve(packageDir, 'build.config.js'))).default()
} catch {}
if (customConfig) {
await customConfig.finalJsr?.({ packageDir, outDir })
}
console.log('[i] Trying to publish with --dry-run')
cp.execSync('deno publish --dry-run --allow-dirty --quiet', { cwd: outDir, stdio: 'inherit' })
console.log('[v] All good!')
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const PACKAGE_NAME = process.argv[2]
if (!PACKAGE_NAME) {
throw new Error('package name not specified')
}
await runJsrBuildSync(PACKAGE_NAME)
}

View file

@ -4,6 +4,7 @@ import { resolve } from 'node:path'
import { processPackageJson } from '../.config/vite-utils/package-json.js' import { processPackageJson } from '../.config/vite-utils/package-json.js'
import { packageJsonToDeno, runJsrBuildSync } from './build-package-jsr.js'
import { runViteBuildSync } from './build-package-vite.js' import { runViteBuildSync } from './build-package-vite.js'
if (process.argv.length < 3) { if (process.argv.length < 3) {
@ -13,16 +14,20 @@ if (process.argv.length < 3) {
const IS_JSR = process.env.JSR === '1' const IS_JSR = process.env.JSR === '1'
if (IS_JSR) {
throw new Error('JSR build is temporarily disabled')
}
const packageName = process.argv[2] const packageName = process.argv[2]
function transformFile(file, transform) {
const content = fs.readFileSync(file, 'utf8')
const res = transform(content, file)
if (res != null) fs.writeFileSync(file, res)
}
if (packageName === 'tl') { if (packageName === 'tl') {
// create package by copying all the needed files // create package by copying all the needed files
const packageDir = fileURLToPath(new URL('../packages/tl', import.meta.url)) const packageDir = fileURLToPath(new URL('../packages/tl', import.meta.url))
const outDir = fileURLToPath(new URL('../packages/tl/dist', import.meta.url)) let outDir = fileURLToPath(new URL('../packages/tl/dist', import.meta.url))
if (IS_JSR) outDir = resolve(outDir, 'jsr')
fs.rmSync(outDir, { recursive: true, force: true }) fs.rmSync(outDir, { recursive: true, force: true })
const files = [ const files = [
@ -48,65 +53,65 @@ if (packageName === 'tl') {
} }
fs.cpSync(new URL('../LICENSE', import.meta.url), resolve(outDir, 'LICENSE'), { recursive: true }) fs.cpSync(new URL('../LICENSE', import.meta.url), resolve(outDir, 'LICENSE'), { recursive: true })
const { packageJson } = processPackageJson(packageDir) const { packageJson, packageJsonOrig } = processPackageJson(packageDir)
fs.writeFileSync(resolve(outDir, 'package.json'), JSON.stringify(packageJson, null, 4))
// todo if (IS_JSR) {
// if (jsr) { // jsr doesn't support cjs, so we'll need to add some shims
// // jsr doesn't support cjs, so we'll need to add some shims // todo: remove this god awfulness when tl esm rewrite
// // todo: remove this god awfulness when tl esm rewrite transformFile(resolve(outDir, 'index.js'), (content) => {
// transformFile(path.join(outDir, 'index.js'), (content) => { return [
// return [ '/// <reference types="./index.d.ts" />',
// '/// <reference types="./index.d.ts" />', 'const exports = {};',
// 'const exports = {};', content,
// content, 'export const tl = exports.tl;',
// 'export const tl = exports.tl;', 'export const mtp = exports.mtp;',
// 'export const mtp = exports.mtp;', ].join('\n')
// ].join('\n') })
// }) transformFile(resolve(outDir, 'binary/reader.js'), (content) => {
// transformFile(path.join(outDir, 'binary/reader.js'), (content) => { return [
// return [ '/// <reference types="./reader.d.ts" />',
// '/// <reference types="./reader.d.ts" />', 'const exports = {};',
// 'const exports = {};', content,
// content, 'export const __tlReaderMap = exports.__tlReaderMap;',
// 'export const __tlReaderMap = exports.__tlReaderMap;', ].join('\n')
// ].join('\n') })
// }) transformFile(resolve(outDir, 'binary/writer.js'), (content) => {
// transformFile(path.join(outDir, 'binary/writer.js'), (content) => { return [
// return [ '/// <reference types="./writer.d.ts" />',
// '/// <reference types="./writer.d.ts" />', 'const exports = {};',
// 'const exports = {};', content,
// content, 'export const __tlWriterMap = exports.__tlWriterMap;',
// 'export const __tlWriterMap = exports.__tlWriterMap;', ].join('\n')
// ].join('\n') })
// }) transformFile(resolve(outDir, 'binary/rsa-keys.js'), (content) => {
// transformFile(path.join(outDir, 'binary/rsa-keys.js'), (content) => { return [
// return [ '/// <reference types="./rsa-keys.d.ts" />',
// '/// <reference types="./rsa-keys.d.ts" />', 'const exports = {};',
// 'const exports = {};', content,
// content, 'export const __publicKeyIndex = exports.__publicKeyIndex;',
// 'export const __publicKeyIndex = exports.__publicKeyIndex;', ].join('\n')
// ].join('\n') })
// })
// // patch deno.json to add some export maps // patch deno.json to add some export maps
// transformFile(path.join(outDir, 'deno.json'), (content) => { const denoJson = packageJsonToDeno({ packageJson, packageJsonOrig })
// const json = JSON.parse(content) denoJson.exports = {}
// json.exports = {}
// for (const f of files) { for (const f of files) {
// if (!f.match(/\.js(?:on)?$/)) continue if (!f.match(/\.js(?:on)?$/)) continue
if (f === 'index.js') {
// if (f === 'index.js') { denoJson.exports['.'] = './index.js'
// json.exports['.'] = './index.js' } else {
// } else { denoJson.exports[`./${f}`] = `./${f}`
// json.exports[`./${f}`] = `./${f}` }
// } }
// } fs.writeFileSync(resolve(outDir, 'deno.json'), JSON.stringify(denoJson, null, 2))
} else {
// return JSON.stringify(json, null, 2) fs.writeFileSync(resolve(outDir, 'package.json'), JSON.stringify(packageJson, null, 2))
// }) }
// }
} else { } else {
if (IS_JSR) {
await runJsrBuildSync(packageName)
} else {
runViteBuildSync(packageName) runViteBuildSync(packageName)
}
} }