UNPKG

make-deno-edition

Version:

Automatically makes package.json projects (such as npm packages and node.js modules) compatible with Deno.

523 lines (522 loc) 18.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.inform = exports.make = exports.convert = exports.importRegExp = void 0; /* eslint new-cap:0, no-loop-func:0, camelcase:0, no-use-before-define:0 */ const fdir_1 = require("fdir"); const errlop_1 = __importDefault(require("errlop")); const rimraf_1 = __importDefault(require("rimraf")); const mkdirp_1 = __importDefault(require("mkdirp")); const path_1 = require("path"); const fs_1 = require("fs"); const await_spawn_1 = __importDefault(require("await-spawn")); const color = __importStar(require("./color.js")); async function rimrafp(p) { return new Promise(function (resolve, reject) { rimraf_1.default(p, function (err) { if (err) return reject(err); resolve(); }); }); } const { readFile, writeFile } = fs_1.promises; async function exists(p) { return new Promise(function (resolve) { fs_1.exists(p, resolve); }); } async function readJSON(path) { return JSON.parse(await readFile(path, 'utf-8')); } async function writeJSON(path, data) { const str = JSON.stringify(data, null, ' '); await writeFile(path, str, 'utf-8'); } async function ensureFile(p, data) { await mkdirp_1.default(path_1.dirname(p)); return await writeFile(p, data); } const trim = [ 'cross-fetch', 'fetch-client', 'fetch-h2', 'fetch-lite', 'isomorphic-fetch', 'isomorphic-unfetch', 'node-fetch', 'unfetch', ]; const perms = [ 'all', 'env', 'hrtime', 'net', 'plugin', 'read', 'run', 'write', ]; // test ground: https://repl.it/@balupton/match-import#index.js // @todo add tests here instead exports.importRegExp = /^(?:import|export(?! (?:async|function|interface|type|class))) .+? from ['"]([^'"]+)['"]$/gms; // https://deno.land/std/node const builtins = { assert: true, buffer: true, child_process: false, cluster: false, console: false, crypto: false, dgram: false, dns: false, events: true, fs: true, http: false, http2: false, https: false, module: true, net: false, os: true, path: true, perf_hooks: false, process: true, querystring: true, readline: false, repl: false, stream: false, string_decoder: false, sys: false, timers: true, tls: false, tty: false, url: true, util: true, v8: false, vm: false, worker_threads: false, zlib: false, }; function replaceImportStatement(sourceStatement, sourceTarget, resultTarget) { if (!resultTarget) return ''; const parts = sourceStatement.split(' '); const lastPart = parts.pop(); const replacement = parts .concat([lastPart.replace(sourceTarget, resultTarget)]) .join(' '); return replacement; } function convert(path, details) { // prepare const file = details.files[path]; // extract imports const matches = file.source.matchAll(exports.importRegExp); for (const match of matches) { const i = { type: null, label: match[1], sourceIndex: match.index, sourceStatement: match[0], sourceTarget: match[1], errors: new Set(), }; file.imports.push(i); } // check the compat of each import for (const i of file.imports) { const { sourceTarget } = i; // check if local dependency, if so, ensure .ts extension // and ensure it is supported itself if (sourceTarget.startsWith('.')) { i.type = 'internal'; // ensure extension if (sourceTarget.endsWith('/')) { i.resultTarget = sourceTarget + 'index.ts'; } else { const ext = path_1.extname(sourceTarget); if (ext === '') { i.resultTarget = sourceTarget + '.ts'; } else if (ext) { i.resultTarget = sourceTarget.replace(ext, '.ts'); } } // check the path i.path = path_1.resolve(path_1.dirname(path), i.resultTarget); i.file = details.files[i.path]; if (!i.file) { i.errors.add(`resolves to [${i.path}] which is not a typescript file inside the source edition`); // skip continue; } // check of i.file.errors happens later // success continue; } // check if remote depednency, if so, ignore if (sourceTarget.startsWith('http:') || sourceTarget.startsWith('https:') || sourceTarget.startsWith('/')) { i.type = 'remote'; i.resultTarget = sourceTarget; continue; } // anything left over must be a dependency i.type = 'dep'; // extract manual entry from package if (sourceTarget.includes('/')) { // custom entry, extract parts const parts = sourceTarget.split('/'); i.package = parts.shift(); // if dep is a scoped package, then include the next part if (i.package[0] === '@') { i.package += '/' + parts.shift(); } // remaining parts will be the manual entry i.entry = parts.join('/'); // actually continue } else { // no custom entry i.package = sourceTarget; } // check if unnecessary if (!i.entry && trim.includes(i.package)) { i.resultTarget = ''; continue; } // check if builtin const compat = builtins[i.package] ?? null; if (!i.entry && compat !== null) { i.type = 'builtin'; // check for compat if (typeof compat === 'string') { i.resultTarget = compat; continue; } else if (compat) { i.resultTarget = `https://deno.land/std/node/${i.package}.ts`; continue; } // fail as the builtin does not yet have a compatibility proxy i.errors.add(`is a node.js builtin that does not yet have a deno compatibility layer`); continue; } // not a builtin, is a dependency, check if installed else { // check if package, if so, check for deno entry, if so use that, otherwise use main i.dep = details.deps[i.package]; if (i.dep) { // apply const entry = i.entry || i.dep.entry || ''; i.resultTarget = i.dep.url + '/' + entry; // check of i.dep.errors happens later // fail if invalid entry if (!entry.endsWith('.ts')) { i.errors.add(`resolved to [${i.package}/${entry}], which does not have the .ts extension`); continue; } } else { // invalid dependency import i.errors.add(`appears to be an uninstalled dependency, install it and try again`); } } } // perform the replacements let result = file.source; let offset = 0; for (const i of file.imports) { i.label = `${i.type} import of [${i.sourceTarget}] => [${i.resultTarget}]`; if (i.resultTarget == null) { // error case continue; } const cursor = i.sourceIndex + offset; const replacement = replaceImportStatement(i.sourceStatement, i.sourceTarget, i.resultTarget); result = result.substring(0, cursor) + replacement + result.substring(cursor + i.sourceStatement.length); offset += replacement.length - i.sourceStatement.length; } // __filename and __dirname ponyfill if (/__(file|dir)name/.test(result) && /__(file|dir)name\s?=/.test(result) === false) { result = `import filedirname from 'https://unpkg.com/filedirname@^2.0.0/edition-deno/index.ts';\n` + `const [ __filename, __dirname ] = filedirname(import.meta.url);\n` + result; } // apply and return file.result = result; return file; } exports.convert = convert; async function make({ run = true, cwd = process.cwd(), failOnEntryIncompatibility = true, failOnTestIncompatibility = false, failOnOtherIncompatibility = false, } = {}) { // paths const pkgPath = path_1.join(cwd, 'package.json'); const pkg = await readJSON(pkgPath).catch((err) => Promise.reject(new errlop_1.default('require package.json file to be present', err))); // prepare const keywords = new Set(pkg.keywords || []); const denoEditionDirectory = 'edition-deno'; const denoEditionPath = path_1.join(cwd, denoEditionDirectory); const nm = path_1.join(cwd, 'node_modules'); // permission args const permArgs = []; for (const perm of perms) { const name = 'allow-' + perm; if (keywords.has(name)) { const arg = '--' + name; permArgs.push(arg); } } // check editions const sourceEdition = pkg?.editions && pkg.editions[0]; if (!sourceEdition || !sourceEdition.tags?.includes('typescript') || !sourceEdition.tags?.includes('import')) { throw new Error('make-deno-edition requires you to define the edition entry for the typescript source code\n' + 'refer to https://github.com/bevry/make-deno-edition and https://editions.bevry.me for details'); } // get the source edition path const sourceEditionPath = path_1.join(cwd, sourceEdition.directory); // get the deno entry const denoEntry = (await exists(path_1.join(sourceEditionPath, 'deno.ts'))) ? 'deno.ts' : sourceEdition.entry; // get the source edition files const api = new fdir_1.fdir() .withFullPaths() .filter((path) => path.endsWith('.ts')) .crawl(sourceEditionPath); const paths = (await api.withPromise()); // delete the old files await rimrafp(denoEditionPath); // prepare details const details = { files: {}, deps: {}, success: true, }; // add the dependencies for (const [name, version] of Object.entries(Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {}))) { if (details.deps[name]) { throw new Error(`[${name}] dependency is duplicated`); } else { const dep = { name, version: version, url: `https://unpkg.com/${name}@${version}`, errors: new Set(), }; const path = path_1.join(nm, name, 'package.json'); try { const pkg = await readJSON(path); const deno = pkg?.deno; const main = pkg?.main; dep.entry = deno || main; } catch (err) { // don't change success, as this dependency may not be actually be used dep.errors.add(`dependency [${name}] does not appear installed, as [${path}] was not valid JSON, install the dependency and try again`); } details.deps[dep.name] = dep; } } // add the files await Promise.all(paths.map(async (path) => { const filename = path.replace(sourceEditionPath + '/', ''); const source = await readFile(path, 'utf-8'); let necessary; let label; if (filename === denoEntry) { necessary = failOnEntryIncompatibility; label = `entry file [${path}]`; } else if (filename.includes('test')) { necessary = failOnTestIncompatibility; label = `test file [${path}]`; } else { necessary = failOnOtherIncompatibility; label = `utility file [${path}]`; } label = (necessary ? 'necessary ' : 'optional ') + label; const file = { label, path, filename, denoPath: path_1.join(denoEditionPath, filename), necessary, source, imports: [], errors: new Set(), }; details.files[path] = file; })); // convert all the files for (const path of Object.keys(details.files)) { convert(path, details); } // bubble nested errors for (const iteration of [1, 2]) { for (const [path, file] of Object.entries(details.files)) { for (const i of file.imports) { // bubble dep import errors if (i.dep?.errors.size) i.errors.add(`import of dependency [${i.dep.name}] has incompatibilities`); // bubble file import errors if (i.file?.errors.size) i.errors.add(`import of local file [${i.sourceTarget}] has incompatibilities`); // bubble import errors if (i.errors.size) file.errors.add(`has import incompatibilities`); } } } // check if we care about the errors or not for (const file of Object.values(details.files)) { if (file.errors.size && file.necessary) { details.success = false; break; } } // if successful, write the new files const denoFiles = []; if (details.success) { for (const file of Object.values(details.files)) { // write the successful files only if (file.errors.size === 0) { if (file.result == null) throw new Error('the file had no errors, yet had no content'); await ensureFile(file.denoPath, file.result); denoFiles.push(file); } } } // attempt to run the successful files if (run) { for (const file of denoFiles) { const args = ['run', ...permArgs, '--reload', '--unstable', file.denoPath]; try { await await_spawn_1.default('deno', args); } catch (err) { file.errors.add(`running deno on the file failed:\n\tdeno ${args.join(' ')}\n\t` + String(err.stderr).replace(/\n/g, '\n\t')); if (file.errors.size && file.necessary) { details.success = false; } } } } // delete deno edition entry, will be re-added later if it is suitable pkg.editions = pkg.editions.filter((e) => e.directory !== denoEditionDirectory); // change package.json for success if (details.success) { // add deno keywords keywords.add('deno'); keywords.add('denoland'); keywords.add('deno-entry'); keywords.add('deno-edition'); pkg.keywords = Array.from(keywords).sort(); // add deno edition const denoEdition = { description: 'TypeScript source code made to be compatible with Deno', directory: denoEditionDirectory, entry: denoEntry, tags: ['typescript', 'import', 'deno'], engines: { deno: true, browsers: Boolean(pkg.browser), }, }; pkg.editions.push(denoEdition); // add deno entry pkg.deno = path_1.join(denoEdition.directory, denoEdition.entry); // save writeJSON(pkgPath, pkg); } // change package.json for failure else { // delete deno keywords keywords.delete('deno'); keywords.delete('denoland'); keywords.delete('deno-entry'); keywords.delete('deno-edition'); pkg.keywords = Array.from(keywords).sort(); // delete deno entry delete pkg.deno; // save writeJSON(pkgPath, pkg); } // return details return details; } exports.make = make; function inform(details, verbose = false) { for (const path of Object.keys(details.files).sort()) { const file = details.files[path]; if (file.errors.size) { if (verbose) { console.log(); } if (file.necessary) { console.log(color.error(file.label, 'failed')); } else { console.log(color.warn(file.label, 'skipped')); } if (verbose) { for (const e of file.errors) { console.log('↳ ', e); for (const i of file.imports) { if (i.errors.size) { console.log(' ↳ ', i.label); for (const e of i.errors) { console.log(' ↳ ', e); } } } } } } else { console.log(); console.log(color.success(file.label, 'passed')); } } // console.log('\ndetected dependencies:') // for (const key of Object.keys(details.deps).sort()) { // const dep = details.deps[key] // console.log(color.inspect(dep)) // } // add dep failures? console.log(); } exports.inform = inform;