221 lines
7.6 KiB
JavaScript
221 lines
7.6 KiB
JavaScript
|
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)
|
||
|
}
|