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

View file

@ -1,5 +1,7 @@
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import { resolve } from 'node:path'
import * as fs from 'node:fs'
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 type { mtp } from '@mtcute/tl'
import { timers } from '../utils'
import { timers } from '../utils/index.js'
export class ServerSaltManager {
private _futureSalts: mtp.RawMt_future_salt[] = []

View file

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

View file

@ -51,7 +51,8 @@ export function initSync(module: SyncInitInput): void {
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()
}

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