finally ESM working with tests!!
This commit is contained in:
parent
16c2723ced
commit
1ed83623e7
|
@ -1,11 +1,15 @@
|
|||
#! /usr/bin/env node
|
||||
const path = require('path')
|
||||
const childProcess = require('child_process')
|
||||
import { join, resolve } from 'path'
|
||||
import { fork } from 'child_process'
|
||||
import minimatch from 'minimatch'
|
||||
import glob from 'glob'
|
||||
import { fileURLToPath } from 'url'
|
||||
const { sync } = glob
|
||||
|
||||
const __dirname = resolve(fileURLToPath(import.meta.url), '../')
|
||||
const rootDir = join(__dirname, '../..')
|
||||
|
||||
const rootDir = path.join(__dirname, '../..')
|
||||
const mochaTsRelativeDir = '.mocha-ts'
|
||||
const minimatch = require('minimatch')
|
||||
const glob = require('glob')
|
||||
|
||||
// First let us prepare the args to pass to mocha.
|
||||
// ts.files will be replaced by their js-transpiled counterparts
|
||||
|
@ -13,13 +17,13 @@ const glob = require('glob')
|
|||
const processedArgs = processArgs(process.argv.slice(2))
|
||||
|
||||
// Now we can run a script and invoke a callback when complete, e.g.
|
||||
runScript(path.join(rootDir, 'node_modules/mocha/bin/mocha'), processedArgs)
|
||||
runScript(join(rootDir, 'node_modules/mocha/bin/mocha'), processedArgs)
|
||||
|
||||
function processArgs (args) {
|
||||
args = process.argv.slice(2).map(arg => {
|
||||
// Let us first remove surrounding quotes in string (it gives issues in windows)
|
||||
arg = arg.replace(/^['"]/, '').replace(/['"]$/, '')
|
||||
const filenames = glob.sync(arg, { cwd: rootDir, matchBase: true })
|
||||
const filenames = sync(arg, { cwd: rootDir, matchBase: true })
|
||||
if (filenames.length > 0) {
|
||||
return filenames.map(file => {
|
||||
const isTsTestFile = minimatch(file, '{test/**/*.ts,src/**/*.spec.ts}', { matchBase: true })
|
||||
|
@ -58,7 +62,7 @@ function processArgs (args) {
|
|||
}
|
||||
|
||||
function runScript (scriptPath, args) {
|
||||
const mochaCmd = childProcess.fork(scriptPath, args, {
|
||||
const mochaCmd = fork(scriptPath, args, {
|
||||
cwd: rootDir
|
||||
})
|
||||
|
|
@ -7,7 +7,7 @@ import typescriptPlugin from '@rollup/plugin-typescript'
|
|||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import json from '@rollup/plugin-json'
|
||||
|
||||
import { join } from 'path'
|
||||
import { dirname, join } from 'path'
|
||||
import { existsSync } from 'fs-extra'
|
||||
import { directories, name as _name, exports } from '../package.json'
|
||||
import { compile } from './rollup-plugin-dts.js'
|
||||
|
@ -145,7 +145,7 @@ export default [
|
|||
typescriptPlugin(tsBundleOptions),
|
||||
resolve({
|
||||
browser: false,
|
||||
exportConditions: ['node', 'module', 'import']
|
||||
exportConditions: ['require', 'node', 'module', 'import']
|
||||
}),
|
||||
commonjs({ extensions: ['.js', '.cjs', '.ts', '.jsx', '.cjsx', '.tsx'] }), // the ".ts" extension is required
|
||||
json()
|
||||
|
@ -163,12 +163,14 @@ export default [
|
|||
plugins: [
|
||||
replace({
|
||||
IS_BROWSER: false,
|
||||
__filename: `'${join(rootDir, exports['.'].node.import)}'`,
|
||||
__dirname: `'${dirname(join(rootDir, exports['.'].node.import))}'`,
|
||||
preventAssignment: true
|
||||
}),
|
||||
typescriptPlugin(tsBundleOptions),
|
||||
resolve({
|
||||
browser: false,
|
||||
exportConditions: ['node', 'module', 'import']
|
||||
exportConditions: ['node']
|
||||
}),
|
||||
commonjs({ extensions: ['.js', '.cjs', '.ts', '.jsx', '.cjsx', '.tsx'] }), // the ".ts" extension is required
|
||||
json()
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
const EventEmitter = require('events')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
import EventEmitter from 'events'
|
||||
import { mkdirSync, writeFileSync, rmSync } from 'fs'
|
||||
import { dirname } from 'path'
|
||||
|
||||
module.exports = class Builder extends EventEmitter {
|
||||
export default class Builder extends EventEmitter {
|
||||
constructor (semaphoreFile, name = 'builder') {
|
||||
super()
|
||||
this.name = name
|
||||
this.firstBuild = true
|
||||
fs.mkdirSync(path.dirname(semaphoreFile), { recursive: true })
|
||||
mkdirSync(dirname(semaphoreFile), { recursive: true })
|
||||
|
||||
this.semaphoreFile = semaphoreFile
|
||||
this._ready = false
|
||||
|
@ -26,7 +26,7 @@ module.exports = class Builder extends EventEmitter {
|
|||
|
||||
this.on('ready', () => {
|
||||
if (this.firstBuild === false) {
|
||||
fs.writeFileSync(this.semaphoreFile, '', 'utf-8')
|
||||
writeFileSync(this.semaphoreFile, '', 'utf-8')
|
||||
} else {
|
||||
this.firstBuild = false
|
||||
}
|
||||
|
@ -54,6 +54,6 @@ module.exports = class Builder extends EventEmitter {
|
|||
async close () {}
|
||||
|
||||
clean () {
|
||||
fs.rmSync(this.semaphoreFile, { force: true })
|
||||
rmSync(this.semaphoreFile, { force: true })
|
||||
}
|
||||
}
|
|
@ -1,18 +1,25 @@
|
|||
const EventEmitter = require('events')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
import EventEmitter from 'events'
|
||||
import { writeFileSync, existsSync } from 'fs'
|
||||
import { join, resolve } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const rollup = require('rollup')
|
||||
const loadAndParseConfigFile = require('rollup/dist/loadConfigFile.js')
|
||||
import { watch as _watch, rollup as _rollup } from 'rollup'
|
||||
import loadAndParseConfigFile from 'rollup/dist/loadConfigFile.js'
|
||||
|
||||
const Builder = require('./Builder.cjs')
|
||||
import Builder from './Builder.js'
|
||||
|
||||
const rootDir = path.join(__dirname, '../../../../')
|
||||
const pkgJson = require(path.join(rootDir, 'package.json'))
|
||||
const __dirname = resolve(fileURLToPath(import.meta.url), '../')
|
||||
|
||||
module.exports = class RollupBuilder extends Builder {
|
||||
constructor ({ name = 'rollup', configPath = path.join(rootDir, 'rollup.config.js'), tempDir = path.join(rootDir, '.mocha-ts'), watch = false }) {
|
||||
super(path.join(tempDir, 'semaphore'), name)
|
||||
const rootDir = join(__dirname, '../../../../')
|
||||
const pkgJson = (await import(join(rootDir, 'package.json'), {
|
||||
assert: {
|
||||
type: "json",
|
||||
}
|
||||
})).default
|
||||
|
||||
export default class RollupBuilder extends Builder {
|
||||
constructor ({ name = 'rollup', configPath = join(rootDir, 'rollup.config.js'), tempDir = join(rootDir, '.mocha-ts'), watch = false }) {
|
||||
super(join(tempDir, 'semaphore'), name)
|
||||
this.configPath = configPath
|
||||
this.watch = watch
|
||||
}
|
||||
|
@ -21,14 +28,16 @@ module.exports = class RollupBuilder extends Builder {
|
|||
await super.start()
|
||||
|
||||
const { options } = await loadAndParseConfigFile(this.configPath)
|
||||
// Watch only the Node CJS module, that is the one we are going to use with mocha
|
||||
// Watch only the Node ESM module, that is the one we are going to use with mocha
|
||||
const rollupOptions = options.filter(bundle => {
|
||||
const file = (bundle.output[0].dir !== undefined)
|
||||
? path.join(bundle.output[0].dir, bundle.output[0].entryFileNames)
|
||||
? join(bundle.output[0].dir, bundle.output[0].entryFileNames)
|
||||
: bundle.output[0].file
|
||||
return file === path.join(rootDir, pkgJson.main)
|
||||
return file === join(rootDir, pkgJson.exports['.'].node.import)
|
||||
})[0]
|
||||
delete rollupOptions.output.pop() // remove the second output
|
||||
if (rollupOptions.output.length > 1) {
|
||||
rollupOptions.output = rollupOptions.output[0]
|
||||
}
|
||||
|
||||
this.builder = new RollupBundler(rollupOptions, this.watch)
|
||||
|
||||
|
@ -55,7 +64,7 @@ module.exports = class RollupBuilder extends Builder {
|
|||
case 'ERROR':
|
||||
if (event.result) event.result.close()
|
||||
this.emit('error', event.error)
|
||||
fs.writeFileSync(path.join(rootDir, pkgJson.main), '', 'utf8')
|
||||
writeFileSync(join(rootDir, pkgJson.exports['.'].node.import), '', 'utf8')
|
||||
this.emit('ready')
|
||||
break
|
||||
|
||||
|
@ -85,13 +94,13 @@ class RollupBundler extends EventEmitter {
|
|||
|
||||
async start () {
|
||||
if (this.watch === true) {
|
||||
this.watcher = rollup.watch(this.rollupOptions)
|
||||
this.watcher = _watch(this.rollupOptions)
|
||||
|
||||
this.watcher.on('event', event => {
|
||||
this.emit('event', event)
|
||||
})
|
||||
} else {
|
||||
if (fs.existsSync(path.join(rootDir, pkgJson.main)) === false) {
|
||||
if (existsSync(join(rootDir, pkgJson.exports['.'].node.import)) === false) {
|
||||
await this._bundle()
|
||||
} else {
|
||||
this.emit('event', { code: 'END', noBuild: true })
|
||||
|
@ -103,7 +112,7 @@ class RollupBundler extends EventEmitter {
|
|||
this.emit('event', { code: 'START' })
|
||||
for (const optionsObj of [].concat(this.rollupOptions)) {
|
||||
try {
|
||||
const bundle = await rollup.rollup(optionsObj)
|
||||
const bundle = await _rollup(optionsObj)
|
||||
try {
|
||||
await Promise.all(optionsObj.output.map(bundle.write))
|
||||
this.emit('event', { code: 'BUNDLE_END' })
|
|
@ -1,108 +0,0 @@
|
|||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const ts = require('typescript')
|
||||
const JSON5 = require('json5')
|
||||
|
||||
const Builder = require('./Builder.cjs')
|
||||
|
||||
const rootDir = path.join(__dirname, '../../../../')
|
||||
const mochaTsRelativeDir = '.mocha-ts'
|
||||
const mochaTsDir = path.join(rootDir, mochaTsRelativeDir)
|
||||
|
||||
const formatHost = {
|
||||
getCanonicalFileName: path => path,
|
||||
getCurrentDirectory: ts.sys.getCurrentDirectory,
|
||||
getNewLine: () => ts.sys.newLine
|
||||
}
|
||||
|
||||
module.exports = class TestsBuilder extends Builder {
|
||||
constructor ({ name = 'tsc', configPath = path.join(rootDir, 'tsconfig.json'), tempDir = mochaTsDir }) {
|
||||
super(path.join(tempDir, 'semaphore'), name)
|
||||
|
||||
if (fs.existsSync(configPath) !== true) throw new Error(`Couldn't find a tsconfig file at ${configPath}`)
|
||||
|
||||
this.tempDir = tempDir
|
||||
|
||||
const tsConfig = JSON5.parse(fs.readFileSync(configPath, 'utf8'))
|
||||
|
||||
tsConfig.file = undefined
|
||||
|
||||
// Exclude already transpiled files in src
|
||||
tsConfig.exclude = ['src/ts/**/!(*.spec).ts']
|
||||
|
||||
// "noResolve": true
|
||||
tsConfig.compilerOptions.noResolve = false
|
||||
|
||||
// we don't need declaration files
|
||||
tsConfig.compilerOptions.declaration = false
|
||||
|
||||
// we need to emit files
|
||||
tsConfig.compilerOptions.noEmit = false
|
||||
|
||||
// source mapping eases debuging
|
||||
tsConfig.compilerOptions.sourceMap = true
|
||||
|
||||
// This prevents SyntaxError: Cannot use import statement outside a module
|
||||
// tsConfig.compilerOptions.module = 'commonjs'
|
||||
|
||||
// Removed typeroots (it causes issues)
|
||||
tsConfig.compilerOptions.typeRoots = undefined
|
||||
|
||||
tsConfig.compilerOptions.outDir = path.isAbsolute(tempDir) ? path.relative(rootDir, tempDir) : tempDir
|
||||
|
||||
this.tempTsConfigPath = path.join(rootDir, '.tsconfig.json')
|
||||
|
||||
fs.writeFileSync(this.tempTsConfigPath, JSON.stringify(tsConfig, undefined, 2))
|
||||
|
||||
const createProgram = ts.createSemanticDiagnosticsBuilderProgram
|
||||
|
||||
const reportDiagnostic = (diagnostic) => {
|
||||
const filePath = path.relative(rootDir, diagnostic.file.fileName)
|
||||
const tranpiledJsPath = `${path.join(tempDir, filePath).slice(0, -3)}.js`
|
||||
const errorLine = diagnostic.file.text.slice(0, diagnostic.start).split(/\r\n|\r|\n/).length
|
||||
if (fs.existsSync(tranpiledJsPath)) {
|
||||
fs.writeFileSync(tranpiledJsPath, '', 'utf8')
|
||||
}
|
||||
this.emit('error', `[Error ${diagnostic.code}]`, `${filePath}:${errorLine}`, ':', ts.flattenDiagnosticMessageText(diagnostic.messageText, formatHost.getNewLine()))
|
||||
}
|
||||
|
||||
const reportWatchStatusChanged = (diagnostic, newLine, options, errorCount) => {
|
||||
if (errorCount !== undefined) {
|
||||
this.emit('ready')
|
||||
} else {
|
||||
this.emit('busy')
|
||||
if (diagnostic.code === 6031) {
|
||||
this.emit('message', 'transpiling your tests...')
|
||||
} else if (diagnostic.code === 6032) {
|
||||
this.emit('message', 'file changes detected. Transpiling your tests...')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note that there is another overload for `createWatchCompilerHost` that takes
|
||||
// a set of root files.
|
||||
this.host = ts.createWatchCompilerHost(
|
||||
this.tempTsConfigPath,
|
||||
{},
|
||||
ts.sys,
|
||||
createProgram,
|
||||
reportDiagnostic,
|
||||
reportWatchStatusChanged
|
||||
)
|
||||
}
|
||||
|
||||
async start () {
|
||||
await super.start()
|
||||
// `createWatchProgram` creates an initial program, watches files, and updates
|
||||
// the program over time.
|
||||
this.watcher = ts.createWatchProgram(this.host)
|
||||
return await this.ready()
|
||||
}
|
||||
|
||||
async close () {
|
||||
await super.close()
|
||||
this.watcher.close()
|
||||
fs.unlinkSync(this.tempTsConfigPath)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'
|
||||
import json5 from 'json5'
|
||||
import { isAbsolute, join, relative, resolve } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import typescript from 'typescript'
|
||||
import Builder from './Builder.js'
|
||||
|
||||
const { parse } = json5
|
||||
|
||||
const { sys, createSemanticDiagnosticsBuilderProgram, flattenDiagnosticMessageText, createWatchCompilerHost, createWatchProgram } = typescript
|
||||
|
||||
const __dirname = resolve(fileURLToPath(import.meta.url), '../')
|
||||
|
||||
const rootDir = join(__dirname, '../../../../')
|
||||
|
||||
const pkgJson = (await import(join(rootDir, 'package.json'), {
|
||||
assert: {
|
||||
type: "json",
|
||||
}
|
||||
})).default
|
||||
|
||||
const mochaTsRelativeDir = '.mocha-ts'
|
||||
const mochaTsDir = join(rootDir, mochaTsRelativeDir)
|
||||
|
||||
const formatHost = {
|
||||
getCanonicalFileName: path => path,
|
||||
getCurrentDirectory: sys.getCurrentDirectory,
|
||||
getNewLine: () => sys.newLine
|
||||
}
|
||||
|
||||
export default class TestsBuilder extends Builder {
|
||||
constructor ({ name = 'tsc', configPath = join(rootDir, 'tsconfig.json'), tempDir = mochaTsDir }) {
|
||||
super(join(tempDir, 'semaphore'), name)
|
||||
|
||||
if (existsSync(configPath) !== true) throw new Error(`Couldn't find a tsconfig file at ${configPath}`)
|
||||
|
||||
this.tempDir = tempDir
|
||||
|
||||
this.tempPkgJsonPath = join(tempDir, 'package.json')
|
||||
|
||||
delete pkgJson.type
|
||||
|
||||
writeFileSync(this.tempPkgJsonPath, JSON.stringify(pkgJson, undefined, 2))
|
||||
|
||||
const tsConfig = parse(readFileSync(configPath, 'utf8'))
|
||||
|
||||
tsConfig.file = undefined
|
||||
|
||||
// Exclude already transpiled files in src
|
||||
tsConfig.exclude = ['src/ts/**/!(*.spec).ts']
|
||||
|
||||
// "noResolve": true
|
||||
tsConfig.compilerOptions.noResolve = false
|
||||
|
||||
// we don't need declaration files
|
||||
tsConfig.compilerOptions.declaration = false
|
||||
|
||||
// we need to emit files
|
||||
tsConfig.compilerOptions.noEmit = false
|
||||
|
||||
// source mapping eases debuging
|
||||
tsConfig.compilerOptions.sourceMap = true
|
||||
|
||||
// This prevents SyntaxError: Cannot use import statement outside a module
|
||||
// tsConfig.compilerOptions.module = 'commonjs'
|
||||
|
||||
// Removed typeroots (it causes issues)
|
||||
tsConfig.compilerOptions.typeRoots = undefined
|
||||
|
||||
tsConfig.compilerOptions.outDir = isAbsolute(tempDir) ? relative(rootDir, tempDir) : tempDir
|
||||
|
||||
this.tempTsConfigPath = join(rootDir, '.tsconfig.json')
|
||||
|
||||
writeFileSync(this.tempTsConfigPath, JSON.stringify(tsConfig, undefined, 2))
|
||||
|
||||
const createProgram = createSemanticDiagnosticsBuilderProgram
|
||||
|
||||
const reportDiagnostic = (diagnostic) => {
|
||||
const filePath = relative(rootDir, diagnostic.file.fileName)
|
||||
const tranpiledJsPath = `${join(tempDir, filePath).slice(0, -3)}.js`
|
||||
const errorLine = diagnostic.file.text.slice(0, diagnostic.start).split(/\r\n|\r|\n/).length
|
||||
if (existsSync(tranpiledJsPath)) {
|
||||
writeFileSync(tranpiledJsPath, '', 'utf8')
|
||||
}
|
||||
this.emit('error', `[Error ${diagnostic.code}]`, `${filePath}:${errorLine}`, ':', flattenDiagnosticMessageText(diagnostic.messageText, formatHost.getNewLine()))
|
||||
}
|
||||
|
||||
const reportWatchStatusChanged = (diagnostic, newLine, options, errorCount) => {
|
||||
if (errorCount !== undefined) {
|
||||
this.emit('ready')
|
||||
} else {
|
||||
this.emit('busy')
|
||||
if (diagnostic.code === 6031) {
|
||||
this.emit('message', 'transpiling your tests...')
|
||||
} else if (diagnostic.code === 6032) {
|
||||
this.emit('message', 'file changes detected. Transpiling your tests...')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note that there is another overload for `createWatchCompilerHost` that takes
|
||||
// a set of root files.
|
||||
this.host = createWatchCompilerHost(
|
||||
this.tempTsConfigPath,
|
||||
{},
|
||||
sys,
|
||||
createProgram,
|
||||
reportDiagnostic,
|
||||
reportWatchStatusChanged
|
||||
)
|
||||
}
|
||||
|
||||
async start () {
|
||||
await super.start()
|
||||
// `createWatchProgram` creates an initial program, watches files, and updates
|
||||
// the program over time.
|
||||
this.watcher = createWatchProgram(this.host)
|
||||
return await this.ready()
|
||||
}
|
||||
|
||||
async close () {
|
||||
await super.close()
|
||||
this.watcher.close()
|
||||
unlinkSync(this.tempTsConfigPath)
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
|
||||
const chai = require('chai')
|
||||
const rimraf = require('rimraf')
|
||||
|
||||
const RollupBuilder = require('./builders/RollupBuilder.cjs')
|
||||
const TestsBuilder = require('./builders/TestsBuilder.cjs')
|
||||
|
||||
require('dotenv').config()
|
||||
|
||||
const rootDir = path.join(__dirname, '../../../')
|
||||
|
||||
global.chai = chai
|
||||
loadPkgToGlobal()
|
||||
|
||||
global.IS_BROWSER = false
|
||||
|
||||
const watch = process.argv.includes('--watch') || process.argv.includes('-w')
|
||||
|
||||
const tempDir = path.join(rootDir, '.mocha-ts')
|
||||
|
||||
const rollupBuilder = new RollupBuilder({ name: 'rollup', configPath: path.join(rootDir, 'build/rollup.config.js'), tempDir, watch })
|
||||
const testBuilder = new TestsBuilder({ name: 'tsc', tempDir })
|
||||
|
||||
rollupBuilder.start() // This should be in exports.mochaGlobalSetup but mocha fails when not in watch mode (DIRT...)
|
||||
testBuilder.start() // This should be in exports.mochaGlobalSetup but mocha fails when not in watch mode (DIRT...)
|
||||
|
||||
exports.mochaHooks = {
|
||||
beforeAll: [
|
||||
async function () {
|
||||
this.timeout('120000')
|
||||
|
||||
await Promise.all([rollupBuilder.ready(), testBuilder.ready()])
|
||||
|
||||
// Just in case our module had been modified. Reload it when the tests are repeated (for mocha watch mode).
|
||||
delete require.cache[require.resolve(rootDir)]
|
||||
loadPkgToGlobal()
|
||||
|
||||
// And now reset any other transpiled module (just delete the cache so it is fully reloaded)
|
||||
for (const key in require.cache) {
|
||||
const relativePath = path.relative(rootDir, key)
|
||||
if (relativePath.startsWith(`.mocha-ts${path.sep}`)) {
|
||||
delete require.cache[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// exports.mochaGlobalSetup = async function () {
|
||||
// await rollupBuilder.start()
|
||||
// await testBuilder.start()
|
||||
// }
|
||||
|
||||
exports.mochaGlobalTeardown = async function () {
|
||||
await testBuilder.close()
|
||||
await rollupBuilder.close()
|
||||
|
||||
// I use the sync version of rimraf precisely because it blocks the
|
||||
// main thread and thus the mocha watcher, which otherwise would complain
|
||||
// about files being deleted
|
||||
rimraf.sync(tempDir, { disableGlob: true })
|
||||
}
|
||||
|
||||
function loadPkgToGlobal () {
|
||||
const _pkg = require(rootDir)
|
||||
if (typeof _pkg === 'function') { // If it is just a default export, load it as named (for compatibility)
|
||||
global._pkg = {
|
||||
default: _pkg
|
||||
}
|
||||
} else {
|
||||
global._pkg = _pkg
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
'use strict'
|
||||
|
||||
import { join, relative, resolve, sep } from 'path'
|
||||
import chai from 'chai'
|
||||
import rimraf from 'rimraf'
|
||||
import { fileURLToPath } from 'url'
|
||||
import RollupBuilder from './builders/RollupBuilder.js'
|
||||
import TestsBuilder from './builders/TestsBuilder.js'
|
||||
import 'dotenv/config'
|
||||
|
||||
const __dirname = resolve(fileURLToPath(import.meta.url), '../')
|
||||
|
||||
const rootDir = join(__dirname, '../../../')
|
||||
|
||||
const pkgJson = (await import(join(rootDir, 'package.json'), {
|
||||
assert: {
|
||||
type: "json",
|
||||
}
|
||||
})).default
|
||||
|
||||
global.chai = chai
|
||||
|
||||
async function reloadModule () {
|
||||
const _pkg = await import(join(rootDir, pkgJson.exports['.'].node.import + `?update=${Date.now()}`))
|
||||
return _pkg
|
||||
// if (typeof _pkg === 'function') { // If it is just a default export, load it as named (for compatibility)
|
||||
// global._pkg = {
|
||||
// default: _pkg
|
||||
// }
|
||||
// } else {
|
||||
// global._pkg = _pkg
|
||||
// }
|
||||
}
|
||||
|
||||
global._pkg = await reloadModule()
|
||||
|
||||
global.IS_BROWSER = false
|
||||
|
||||
const watch = process.argv.includes('--watch') || process.argv.includes('-w')
|
||||
|
||||
const tempDir = join(rootDir, '.mocha-ts')
|
||||
|
||||
const rollupBuilder = new RollupBuilder({ name: 'rollup', configPath: join(rootDir, 'build/rollup.config.js'), tempDir, watch })
|
||||
const testBuilder = new TestsBuilder({ name: 'tsc', tempDir })
|
||||
|
||||
rollupBuilder.start() // This should be in exports.mochaGlobalSetup but mocha fails when not in watch mode (DIRT...)
|
||||
testBuilder.start() // This should be in exports.mochaGlobalSetup but mocha fails when not in watch mode (DIRT...)
|
||||
|
||||
export const mochaHooks = {
|
||||
beforeAll: [
|
||||
async function () {
|
||||
this.timeout('120000')
|
||||
|
||||
await Promise.all([rollupBuilder.ready(), testBuilder.ready()])
|
||||
|
||||
// Just in case our module had been modified. Reload it when the tests are repeated (for mocha watch mode).
|
||||
// delete require.cache[require.resolve(rootDir)]
|
||||
global._pkg = await reloadModule()
|
||||
|
||||
// And now reset any other transpiled module (just delete the cache so it is fully reloaded)
|
||||
// for (const key in require.cache) {
|
||||
// const relativePath = relative(rootDir, key)
|
||||
// if (relativePath.startsWith(`.mocha-ts${sep}`)) {
|
||||
// delete require.cache[key]
|
||||
// }
|
||||
// }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// exports.mochaGlobalSetup = async function () {
|
||||
// await rollupBuilder.start()
|
||||
// await testBuilder.start()
|
||||
// }
|
||||
|
||||
export const mochaGlobalTeardown = async function () {
|
||||
await testBuilder.close()
|
||||
await rollupBuilder.close()
|
||||
|
||||
// I use the sync version of rimraf precisely because it blocks the
|
||||
// main thread and thus the mocha watcher, which otherwise would complain
|
||||
// about files being deleted
|
||||
rimraf.sync(tempDir, { disableGlob: true })
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
16
docs/API.md
16
docs/API.md
|
@ -155,7 +155,7 @@ A promise that resolves to a boolean that is either true (a probably prime numbe
|
|||
|
||||
#### Defined in
|
||||
|
||||
[src/ts/isProbablyPrime.ts:21](https://github.com/juanelas/bigint-crypto-utils/blob/f25bb8c/src/ts/isProbablyPrime.ts#L21)
|
||||
[src/ts/isProbablyPrime.ts:21](https://github.com/juanelas/bigint-crypto-utils/blob/16c2723/src/ts/isProbablyPrime.ts#L21)
|
||||
|
||||
___
|
||||
|
||||
|
@ -322,7 +322,7 @@ A promise that resolves to a bigint probable prime of bitLength bits.
|
|||
|
||||
#### Defined in
|
||||
|
||||
[src/ts/prime.ts:25](https://github.com/juanelas/bigint-crypto-utils/blob/f25bb8c/src/ts/prime.ts#L25)
|
||||
[src/ts/prime.ts:25](https://github.com/juanelas/bigint-crypto-utils/blob/16c2723/src/ts/prime.ts#L25)
|
||||
|
||||
___
|
||||
|
||||
|
@ -352,7 +352,7 @@ A bigint probable prime of bitLength bits.
|
|||
|
||||
#### Defined in
|
||||
|
||||
[src/ts/prime.ts:102](https://github.com/juanelas/bigint-crypto-utils/blob/f25bb8c/src/ts/prime.ts#L102)
|
||||
[src/ts/prime.ts:103](https://github.com/juanelas/bigint-crypto-utils/blob/16c2723/src/ts/prime.ts#L103)
|
||||
|
||||
___
|
||||
|
||||
|
@ -381,7 +381,7 @@ A cryptographically secure random bigint between [min,max]
|
|||
|
||||
#### Defined in
|
||||
|
||||
[src/ts/randBetween.ts:15](https://github.com/juanelas/bigint-crypto-utils/blob/f25bb8c/src/ts/randBetween.ts#L15)
|
||||
[src/ts/randBetween.ts:15](https://github.com/juanelas/bigint-crypto-utils/blob/16c2723/src/ts/randBetween.ts#L15)
|
||||
|
||||
___
|
||||
|
||||
|
@ -410,7 +410,7 @@ A Promise that resolves to a UInt8Array/Buffer (Browser/Node.js) filled with cry
|
|||
|
||||
#### Defined in
|
||||
|
||||
[src/ts/randBits.ts:14](https://github.com/juanelas/bigint-crypto-utils/blob/f25bb8c/src/ts/randBits.ts#L14)
|
||||
[src/ts/randBits.ts:14](https://github.com/juanelas/bigint-crypto-utils/blob/16c2723/src/ts/randBits.ts#L14)
|
||||
|
||||
___
|
||||
|
||||
|
@ -439,7 +439,7 @@ A Uint8Array/Buffer (Browser/Node.js) filled with cryptographically secure rando
|
|||
|
||||
#### Defined in
|
||||
|
||||
[src/ts/randBits.ts:45](https://github.com/juanelas/bigint-crypto-utils/blob/f25bb8c/src/ts/randBits.ts#L45)
|
||||
[src/ts/randBits.ts:45](https://github.com/juanelas/bigint-crypto-utils/blob/16c2723/src/ts/randBits.ts#L45)
|
||||
|
||||
___
|
||||
|
||||
|
@ -468,7 +468,7 @@ A promise that resolves to a UInt8Array/Buffer (Browser/Node.js) filled with cry
|
|||
|
||||
#### Defined in
|
||||
|
||||
[src/ts/randBytes.ts:14](https://github.com/juanelas/bigint-crypto-utils/blob/f25bb8c/src/ts/randBytes.ts#L14)
|
||||
[src/ts/randBytes.ts:14](https://github.com/juanelas/bigint-crypto-utils/blob/16c2723/src/ts/randBytes.ts#L14)
|
||||
|
||||
___
|
||||
|
||||
|
@ -497,7 +497,7 @@ A UInt8Array/Buffer (Browser/Node.js) filled with cryptographically secure rando
|
|||
|
||||
#### Defined in
|
||||
|
||||
[src/ts/randBytes.ts:47](https://github.com/juanelas/bigint-crypto-utils/blob/f25bb8c/src/ts/randBytes.ts#L47)
|
||||
[src/ts/randBytes.ts:46](https://github.com/juanelas/bigint-crypto-utils/blob/16c2723/src/ts/randBytes.ts#L46)
|
||||
|
||||
___
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const bigintCryptoUtils = require('../types')
|
||||
const bigintCryptoUtils = require('..')
|
||||
// const bigintCryptoUtils = require('bigint-crypto-utils')
|
||||
|
||||
/* A BigInt with value 666 can be declared calling the bigint constructor as
|
|
@ -0,0 +1,38 @@
|
|||
const bigintCryptoUtils = await import('../dist/esm/index.node.js')
|
||||
// const bigintCryptoUtils = require('bigint-crypto-utils')
|
||||
|
||||
/* A BigInt with value 666 can be declared calling the bigint constructor as
|
||||
BigInt('666') or with the shorter 666n.
|
||||
Notice that you can also pass a number to the constructor, e.g. BigInt(666).
|
||||
However, it is not recommended since values over 2**53 - 1 won't be safe but
|
||||
no warning will be raised.
|
||||
*/
|
||||
const a = BigInt('5')
|
||||
const b = BigInt('2')
|
||||
const n = 19n
|
||||
|
||||
console.log(bigintCryptoUtils.modPow(a, b, n)) // prints 6
|
||||
|
||||
console.log(bigintCryptoUtils.modInv(2n, 5n)) // prints 3
|
||||
|
||||
console.log(bigintCryptoUtils.modInv(BigInt('3'), BigInt('5'))) // prints 2
|
||||
|
||||
console.log(bigintCryptoUtils.randBetween(2n ** 256n)) // Prints a cryptographically secure random number between 1 and 2**256 bits.
|
||||
|
||||
async function primeTesting () {
|
||||
// Output of a probable prime of 2048 bits
|
||||
console.log(await bigintCryptoUtils.prime(2048))
|
||||
|
||||
// Testing if a number is a probable prime (Miller-Rabin)
|
||||
const number = 13139188972124309083000292697519085211422620620787723340749020496498012413131881656428777288953095338604061035790562501399090389032827482643578651715752317n
|
||||
const isPrime = await bigintCryptoUtils.isProbablyPrime(number)
|
||||
if (isPrime) {
|
||||
console.log(`${number} is prime`)
|
||||
} else {
|
||||
console.log(`${number} is composite`)
|
||||
}
|
||||
}
|
||||
|
||||
primeTesting()
|
||||
|
||||
export {}
|
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
|
@ -53,12 +53,12 @@
|
|||
"scripts": {
|
||||
"build": "run-s lint build:js docs",
|
||||
"build:js": "rollup -c build/rollup.config.js",
|
||||
"clean": "rimraf .nyc_output .mocha-ts coverage dist types docs",
|
||||
"coverage": "nyc --check-coverage --exclude build --exclude '{src/ts/**/*.spec.ts,test/**/*.ts,.mocha-ts/**/*}' --reporter=text --reporter=lcov node ./build/bin/mocha-ts.cjs --require build/testing/mocha/mocha-init.cjs '{src/ts/**/*.spec.ts,test/**/*.ts}'",
|
||||
"clean": "rimraf .mocha-ts coverage dist types docs",
|
||||
"coverage": "c8 --clean --check-coverage --exclude-after-remap --exclude '{build,node_modules,src/ts/**/*.spec.ts,test/**/*.ts,.mocha-ts/**/*}' --reporter=text --reporter=lcov node ./build/bin/mocha-ts.js --require build/testing/mocha/mocha-init.js '{src/ts/**/*.spec.ts,test/**/*.ts}'",
|
||||
"docs": "node build/build.docs.cjs",
|
||||
"git:add": "git add -A",
|
||||
"lint": "ts-standard --fix",
|
||||
"mocha-ts": "node ./build/bin/mocha-ts.cjs --require build/testing/mocha/mocha-init.cjs ",
|
||||
"mocha-ts": "node ./build/bin/mocha-ts.js --require build/testing/mocha/mocha-init.js ",
|
||||
"mocha-ts:browser": "node build/testing/browser/index.cjs ",
|
||||
"mocha-ts:browser-headless": "node build/testing/browser/index.cjs headless ",
|
||||
"preversion": "run-s clean lint build:js coverage test:browser-headless",
|
||||
|
@ -68,7 +68,7 @@
|
|||
"test:browser": "npm run mocha-ts:browser ",
|
||||
"test:browser-headless": "npm run mocha-ts:browser-headless ",
|
||||
"test:node": "npm run mocha-ts -- '{src/ts/**/*.spec.ts,test/**/*.ts}'",
|
||||
"watch": "npm run mocha-ts:node -- --watch '{src/ts/**/*.spec.ts,test/**/*.ts}'"
|
||||
"watch": "npm run mocha-ts -- --watch '{src/ts/**/*.spec.ts,test/**/*.ts}'"
|
||||
},
|
||||
"ts-standard": {
|
||||
"env": [
|
||||
|
@ -85,11 +85,12 @@
|
|||
"_pkg",
|
||||
"chai"
|
||||
],
|
||||
"project": "./tsconfig.json",
|
||||
"project": "tsconfig.json",
|
||||
"ignore": [
|
||||
"dist/**/*",
|
||||
"examples/**/*",
|
||||
"types/**/*"
|
||||
"types/**/*",
|
||||
"build/testing/mocha/**/*"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -101,6 +102,7 @@
|
|||
"@rollup/plugin-typescript": "^8.2.0",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"c8": "^7.12.0",
|
||||
"chai": "^4.3.3",
|
||||
"dotenv": "^16.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
|
@ -109,7 +111,6 @@
|
|||
"minimatch": "^5.0.1",
|
||||
"mocha": "^10.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nyc": "^15.1.0",
|
||||
"pirates": "^4.0.1",
|
||||
"puppeteer": "^15.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
|
|
|
@ -25,7 +25,6 @@ export function isProbablyPrime (w: number|bigint, iterations: number = 16, disa
|
|||
if (w < 0n) throw RangeError('w MUST be >= 0')
|
||||
|
||||
if (!IS_BROWSER) { // Node.js
|
||||
/* istanbul ignore else */
|
||||
if (!disableWorkers && _useWorkers) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new workerThreads.Worker(__filename)
|
||||
|
@ -400,7 +399,6 @@ export function _isProbablyPrimeWorkerUrl (): string {
|
|||
if (!IS_BROWSER && _useWorkers) { // node.js with support for workers
|
||||
var workerThreads = await import('worker_threads') // eslint-disable-line
|
||||
const isWorker = !(workerThreads.isMainThread)
|
||||
/* istanbul ignore if */
|
||||
if (isWorker && workerThreads.parentPort !== null) { // worker
|
||||
workerThreads.parentPort.on('message', function (data: MainToWorkerMsg) { // Let's start once we are called
|
||||
const isPrime = _isProbablyPrime(data.rnd, data.iterations)
|
||||
|
|
|
@ -25,7 +25,7 @@ if (!IS_BROWSER) var workerThreads = await import('worker_threads') // eslint-di
|
|||
export function prime (bitLength: number, iterations: number = 16): Promise<bigint> { // eslint-disable-line
|
||||
if (bitLength < 1) throw new RangeError('bitLength MUST be > 0')
|
||||
|
||||
/* istanbul ignore if */
|
||||
/* c8 ignore start */
|
||||
if (!_useWorkers) { // If there is no support for workers
|
||||
let rnd = 0n
|
||||
do {
|
||||
|
@ -33,6 +33,7 @@ export function prime (bitLength: number, iterations: number = 16): Promise<bigi
|
|||
} while (!_isProbablyPrime(rnd, iterations))
|
||||
return new Promise((resolve) => { resolve(rnd) })
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
return new Promise((resolve, reject) => {
|
||||
const workerList: Array<NodeWorker | Worker> = []
|
||||
const _onmessage = (msg: WorkerToMainMsg, newWorker: Worker | NodeWorker): void => {
|
||||
|
|
|
@ -17,7 +17,6 @@ export function randBytes (byteLength: number, forceLength = false): Promise<Uin
|
|||
return new Promise(function (resolve, reject) {
|
||||
if (!IS_BROWSER) {
|
||||
crypto.randomBytes(byteLength, function (err, buf: Buffer) {
|
||||
/* istanbul ignore if */
|
||||
if (err !== null) reject(err)
|
||||
// If fixed length is required we put the first bit to 1 -> to get the necessary bitLength
|
||||
if (forceLength) buf[0] = buf[0] | 128
|
||||
|
|
|
@ -10,13 +10,13 @@ if (!IS_BROWSER) { // Node.js
|
|||
try {
|
||||
await import('worker_threads')
|
||||
_useWorkers = true
|
||||
} catch (e) {
|
||||
/* istanbul ignore next */
|
||||
} /* c8 ignore start */ catch (e) {
|
||||
console.log(`[bigint-crypto-utils] WARNING:
|
||||
This node version doesn't support worker_threads. You should enable them in order to greatly speedup the generation of big prime numbers.
|
||||
· With Node >=11 it is enabled by default (consider upgrading).
|
||||
· With Node 10, starting with 10.5.0, you can enable worker_threads at runtime executing node --experimental-worker `)
|
||||
}
|
||||
/* c8 ignore stop */
|
||||
} else { // Native JS
|
||||
if (self.Worker !== undefined) _useWorkers = true
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||
"module": "esnext",
|
||||
"module": "ESNext",
|
||||
// "lib": [ "es2020" ], /* Specify library files to be included in the compilation. */
|
||||
"allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "outDir": ".dst", /* If not set we cannot import .js files without a warning that is going to be overwritten. outDir is not going to be used in any case */
|
||||
"outDir": ".dst", /* If not set we cannot import .js files without a warning that is going to be overwritten. outDir is not going to be used in any case */
|
||||
"checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'react', 'react-jsx', 'react-jsxdev', 'preserve' or 'react-native'. */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
|
@ -22,12 +22,12 @@
|
|||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
"typeRoots": [ "./build/typings", "./node_modules/@types" ], /* List of folders to include type definitions from. */
|
||||
"typeRoots": [ "node_modules/@types", "build/typings" ], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
"allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
|
@ -35,7 +35,8 @@
|
|||
|
||||
/* Advanced Options */
|
||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/ts/**/*", "build/typings/**/*", "test/**/*"]
|
||||
"include": ["src/ts/**/*", "test/**/*", "build/typings/**/*.d.ts"]
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"isProbablyPrime.d.ts","sourceRoot":"","sources":["../src/ts/isProbablyPrime.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAE,CAAC,EAAE,MAAM,GAAC,MAAM,EAAE,UAAU,GAAE,MAAW,EAAE,cAAc,GAAE,OAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAoD7H;AAED,wBAAgB,gBAAgB,CAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CA0TxE;AAED,wBAAgB,yBAAyB,IAAK,MAAM,CAOnD"}
|
||||
{"version":3,"file":"isProbablyPrime.d.ts","sourceRoot":"","sources":["../src/ts/isProbablyPrime.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAE,CAAC,EAAE,MAAM,GAAC,MAAM,EAAE,UAAU,GAAE,MAAW,EAAE,cAAc,GAAE,OAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAmD7H;AAED,wBAAgB,gBAAgB,CAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CA0TxE;AAED,wBAAgB,yBAAyB,IAAK,MAAM,CAOnD"}
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"prime.d.ts","sourceRoot":"","sources":["../src/ts/prime.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,KAAK,CAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAE,MAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CA+DlF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAE,MAAW,GAAG,MAAM,CAO7E"}
|
||||
{"version":3,"file":"prime.d.ts","sourceRoot":"","sources":["../src/ts/prime.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,KAAK,CAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAE,MAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAgElF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAE,MAAW,GAAG,MAAM,CAO7E"}
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"randBytes.d.ts","sourceRoot":"","sources":["../src/ts/randBytes.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAE,UAAU,EAAE,MAAM,EAAE,WAAW,UAAQ,GAAG,OAAO,CAAC,UAAU,GAAC,MAAM,CAAC,CAoB9F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAE,OAAe,GAAG,UAAU,GAAC,MAAM,CAiBlG"}
|
||||
{"version":3,"file":"randBytes.d.ts","sourceRoot":"","sources":["../src/ts/randBytes.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAE,UAAU,EAAE,MAAM,EAAE,WAAW,UAAQ,GAAG,OAAO,CAAC,UAAU,GAAC,MAAM,CAAC,CAmB9F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAE,OAAe,GAAG,UAAU,GAAC,MAAM,CAiBlG"}
|
Loading…
Reference in New Issue