diff --git a/cmd/index.js b/cmd/index.js index c6e458ce8..f3f765d8e 100644 --- a/cmd/index.js +++ b/cmd/index.js @@ -48,11 +48,12 @@ module.exports = async (ipc, argv = Bare.argv.slice(1)) => { summary('Create initial project files'), description` Links: - pear://electron/template - ${ansi.italic(ansi.dim('pear://your.key.here/your/path/here'))} + pear://templates/terminal/default + pear://templates/desktop/electron + pear://templates/modules/bare + ${ansi.italic(ansi.dim('pear:///path/to/template'))} - Names: - default, ui, node-compat + Names: default, ui `, arg('[link|name]', 'Link or core template to init from'), arg('[dir]', 'Project directory path (default: .)'), diff --git a/cmd/init.js b/cmd/init.js index eca973f29..8aeed8ddd 100644 --- a/cmd/init.js +++ b/cmd/init.js @@ -3,6 +3,19 @@ const fsp = require('bare-fs/promises') const os = require('bare-os') const { basename, resolve } = require('bare-path') const { ansi, outputter, permit } = require('pear-terminal') +const { pipelinePromise, Readable } = require('streamx') +const { pathToFileURL } = require('url-file-url') +const Localdrive = require('localdrive') +const { Interact } = require('pear-terminal') +const stamp = require('pear-stamp') +const plink = require('pear-link') + +const { + ERR_PERMISSION_REQUIRED, + ERR_OPERATION_FAILED, + ERR_DIR_NONEMPTY, + ERR_INVALID_TEMPLATE +} = require('pear-errors') const output = outputter('init', { writing: () => '', @@ -43,7 +56,7 @@ module.exports = (ipc) => try { await output( false, - await require('../init')(link, dir, { + await render(link, dir, { cwd, ipc, autosubmit: yes, @@ -62,3 +75,121 @@ module.exports = (ipc) => await ipc.close() } } + +async function render(link = 'default', dir, opts = {}) { + const { cwd, ipc, header, autosubmit, defaults, force = false, pkg } = opts + let { ask = true } = opts + const isPear = link.startsWith('pear://') + const isFile = link.startsWith('file://') + const isPath = + link[0] === '.' || + link[0] === '/' || + link[1] === ':' || + link.startsWith('\\') + const isName = !isPear && !isFile && !isPath + + if (isName) { + const map = new Map( + Object.entries({ + ui: 'pear://templates/ui/electron', + default: 'pear://templates/terminal/default' + }) + ) + if (map.has(link)) { + link = map.get(link) + ask = false + } + } + + let params = null + if (isPear && ask) { + if ((await ipc.trusted(link)) === false) { + const { drive } = plink.parse(link) + throw ERR_PERMISSION_REQUIRED('Permission required to use template', { + key: drive.key + }) + } + } + + if (isPath) { + let url = pathToFileURL(cwd).toString() + if (url.slice(1) !== '/') url += '/' + link = new URL(link, url).toString() + } + + for await (const { tag, data } of ipc.dump({ + link: link + '/_template.json', + dir: '-' + })) { + if (tag === 'error' && data.code === 'ERR_PERMISSION_REQUIRED') { + throw ERR_PERMISSION_REQUIRED(data.message, data.info) + } + if (tag !== 'file') continue + try { + const definition = JSON.parse(data.value) + params = definition.params + for (const prompt of params) { + defaults[prompt.name] = Array.isArray(prompt.override) + ? prompt.override.reduce((o, k) => o?.[k], pkg) + : (prompt.default ?? defaults[prompt.name]) + if (typeof prompt.validation !== 'string') continue + prompt.validation = new Function( + 'value', + 'return (' + prompt.validation + ')(value)' + ) // eslint-disable-line + } + } catch { + params = null + } + break + } + if (params === null) + throw ERR_INVALID_TEMPLATE('Invalid Template or Unreachable Link') + const dst = new Localdrive(dir) + if (force === false) { + let empty = true + for await (const entry of dst.list()) { + if (entry) { + empty = false + break + } + } + if (empty === false) + throw ERR_DIR_NONEMPTY('Dir is not empty. To overwrite: --force') + } + const output = new Readable({ objectMode: true }) + const prompt = new Interact(header, params, { defaults }) + const { fields, shave } = await prompt.run({ autosubmit }) + output.push({ tag: 'writing' }) + const promises = [] + for await (const { tag, data } of ipc.dump({ link, dir: '-' })) { + if (tag === 'error') { + throw ERR_OPERATION_FAILED('Dump Failed: ' + data.stack) + } + if (tag !== 'file') continue + const { key, value = null } = data + if (key === '/_template.json') continue + if (value === null) continue // dir + const file = stamp.sync(key, fields) + const writeStream = dst.createWriteStream(file) + const promise = pipelinePromise( + stamp.stream(value, fields, shave), + writeStream + ) + promise.catch((err) => { + output.push({ tag: 'error', data: err }) + }) + promise.then(() => { + output.push({ tag: 'wrote', data: { path: file } }) + }) + promises.push(promise) + } + + Promise.allSettled(promises).then((results) => { + const success = results.every(({ status }) => status === 'fulfilled') + output.push({ tag: 'written' }) + output.push({ tag: 'final', data: { success } }) + output.push(null) + }) + return output +} diff --git a/init/index.js b/init/index.js deleted file mode 100644 index 783d800a6..000000000 --- a/init/index.js +++ /dev/null @@ -1,139 +0,0 @@ -'use strict' -const { pipelinePromise, Readable } = require('streamx') -const { pathToFileURL } = require('url-file-url') -const path = require('bare-path') -const Localdrive = require('localdrive') -const { Interact } = require('pear-terminal') -const stamp = require('pear-stamp') -const plink = require('pear-link') -const { LOCALDEV } = require('pear-constants') -const { - ERR_PERMISSION_REQUIRED, - ERR_OPERATION_FAILED, - ERR_DIR_NONEMPTY, - ERR_INVALID_TEMPLATE -} = require('pear-errors') -async function init(link = 'default', dir, opts = {}) { - const { cwd, ipc, header, autosubmit, defaults, force = false, pkg } = opts - let { ask = true } = opts - const isPear = link.startsWith('pear://') - const isFile = link.startsWith('file://') - const isPath = - link[0] === '.' || - link[0] === '/' || - link[1] === ':' || - link.startsWith('\\') - const isName = !isPear && !isFile && !isPath - - if (isName) { - if (link === 'ui') { - link = 'pear://electron/template' - ask = false - } else if (link === 'default' || link === 'node-compat') { - if (LOCALDEV) link = path.join(__dirname, 'templates', link) - else { - const { platform } = await ipc.versions() - link = plink.serialize({ - drive: platform, - pathname: '/init/templates/' + link - }) - } - } else { - return init('./' + link, dir, opts) - } - } - - let params = null - if (isPear && ask) { - if ((await ipc.trusted(link)) === false) { - const { drive } = plink.parse(link) - throw ERR_PERMISSION_REQUIRED('Permission required to use template', { - key: drive.key - }) - } - } - - if (isPath) { - let url = pathToFileURL(cwd).toString() - if (url.slice(1) !== '/') url += '/' - link = new URL(link, url).toString() - } - - for await (const { tag, data } of ipc.dump({ - link: link + '/_template.json', - dir: '-' - })) { - if (tag === 'error' && data.code === 'ERR_PERMISSION_REQUIRED') { - throw ERR_PERMISSION_REQUIRED(data.message, data.info) - } - if (tag !== 'file') continue - try { - const definition = JSON.parse(data.value) - params = definition.params - for (const prompt of params) { - defaults[prompt.name] = Array.isArray(prompt.override) - ? prompt.override.reduce((o, k) => o?.[k], pkg) - : (prompt.default ?? defaults[prompt.name]) - if (typeof prompt.validation !== 'string') continue - prompt.validation = new Function( - 'value', - 'return (' + prompt.validation + ')(value)' - ) // eslint-disable-line - } - } catch { - params = null - } - break - } - if (params === null) - throw ERR_INVALID_TEMPLATE('Invalid Template or Unreachable Link') - const dst = new Localdrive(dir) - if (force === false) { - let empty = true - for await (const entry of dst.list()) { - if (entry) { - empty = false - break - } - } - if (empty === false) - throw ERR_DIR_NONEMPTY('Dir is not empty. To overwrite: --force') - } - const output = new Readable({ objectMode: true }) - const prompt = new Interact(header, params, { defaults }) - const { fields, shave } = await prompt.run({ autosubmit }) - output.push({ tag: 'writing' }) - const promises = [] - for await (const { tag, data } of ipc.dump({ link, dir: '-' })) { - if (tag === 'error') { - throw ERR_OPERATION_FAILED('Dump Failed: ' + data.stack) - } - if (tag !== 'file') continue - const { key, value = null } = data - if (key === '/_template.json') continue - if (value === null) continue // dir - const file = stamp.sync(key, fields) - const writeStream = dst.createWriteStream(file) - const promise = pipelinePromise( - stamp.stream(value, fields, shave), - writeStream - ) - promise.catch((err) => { - output.push({ tag: 'error', data: err }) - }) - promise.then(() => { - output.push({ tag: 'wrote', data: { path: file } }) - }) - promises.push(promise) - } - - Promise.allSettled(promises).then((results) => { - const success = results.every(({ status }) => status === 'fulfilled') - output.push({ tag: 'written' }) - output.push({ tag: 'final', data: { success } }) - output.push(null) - }) - return output -} - -module.exports = init diff --git a/init/templates/default/__main__ b/init/templates/default/__main__ deleted file mode 100644 index d620aaa26..000000000 --- a/init/templates/default/__main__ +++ /dev/null @@ -1,4 +0,0 @@ -/** @typedef {import('pear-interface')} */ /* global Pear */ -const { versions } = Pear -console.log('Pear terminal application running') -console.log(await versions()) diff --git a/init/templates/default/_template.json b/init/templates/default/_template.json deleted file mode 100644 index 5ca83e42f..000000000 --- a/init/templates/default/_template.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "params": [ - { - "name": "name", - "prompt": "name" - }, - { - "name": "main", - "default": "index.js", - "prompt": "main", - "validation": "(value) => value.endsWith('.js')", - "msg": "must have a .js file extension" - }, - { - "name": "license", - "default": "Apache-2.0", - "prompt": "license" - } - ] -} \ No newline at end of file diff --git a/init/templates/default/package.json b/init/templates/default/package.json deleted file mode 100644 index 58434d1e8..000000000 --- a/init/templates/default/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "__name__", - "main": "__main__", - "pear": { - "name": "__name__", - "type": "terminal" - }, - "type": "module", - "license": "__license__", - "scripts": { - "dev": "pear run -d .", - "test": "brittle test/*.test.js" - }, - "devDependencies": { - "brittle": "^3.0.0", - "pear-interface": "^1.0.0" - } -} \ No newline at end of file diff --git a/init/templates/default/test/index.test.js b/init/templates/default/test/index.test.js deleted file mode 100644 index d9b912ddc..000000000 --- a/init/templates/default/test/index.test.js +++ /dev/null @@ -1 +0,0 @@ -import test from 'brittle' // https://github.com/holepunchto/brittle diff --git a/init/templates/node-compat/__main__ b/init/templates/node-compat/__main__ deleted file mode 100644 index 0d2010330..000000000 --- a/init/templates/node-compat/__main__ +++ /dev/null @@ -1,5 +0,0 @@ -/** @typedef {import('pear-interface')} */ /* global Pear */ -require('./compat') // keep at top -const { versions } = Pear -console.log('Pear terminal application running') -console.log(await versions()) diff --git a/init/templates/node-compat/_template.json b/init/templates/node-compat/_template.json deleted file mode 100644 index 5ca83e42f..000000000 --- a/init/templates/node-compat/_template.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "params": [ - { - "name": "name", - "prompt": "name" - }, - { - "name": "main", - "default": "index.js", - "prompt": "main", - "validation": "(value) => value.endsWith('.js')", - "msg": "must have a .js file extension" - }, - { - "name": "license", - "default": "Apache-2.0", - "prompt": "license" - } - ] -} \ No newline at end of file diff --git a/init/templates/node-compat/compat.js b/init/templates/node-compat/compat.js deleted file mode 100644 index 3da75e878..000000000 --- a/init/templates/node-compat/compat.js +++ /dev/null @@ -1,3 +0,0 @@ -global.fetch = require('bare-node-fetch') -global.process = require('bare-node-process') -global.Buffer = require('bare-node-buffer') diff --git a/init/templates/node-compat/package.json b/init/templates/node-compat/package.json deleted file mode 100644 index 34dfa5fd9..000000000 --- a/init/templates/node-compat/package.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "name": "__name__", - "main": "index.js", - "pear": { - "name": "__name__", - "type": "terminal" - }, - "type": "module", - "license": "__license__", - "scripts": { - "dev": "pear run -d .", - "test": "brittle test/*.test.js" - }, - "dependencies": { - "assert": "npm:bare-node-assert", - "async_hooks": "npm:bare-node-async-hooks@^0.0.1", - "bare-assert": "^1.0.2", - "bare-buffer": "^3.1.4", - "bare-console": "^6.0.1", - "bare-crypto": "^1.4.3", - "bare-dgram": "^1.0.1", - "bare-dns": "^2.0.5", - "bare-events": "^2.5.4", - "bare-fs": "^4.1.4", - "bare-http1": "^4.0.2", - "bare-https": "^2.0.0", - "bare-inspector": "^4.0.1", - "bare-module": "^4.8.5", - "bare-net": "^2.0.1", - "bare-node-fetch": "^1.0.0", - "bare-os": "^3.6.1", - "bare-path": "^3.0.0", - "bare-process": "^4.2.1", - "bare-querystring": "^1.0.0", - "bare-readline": "^1.0.9", - "bare-repl": "^4.0.0", - "bare-stream": "^2.6.5", - "bare-subprocess": "^5.0.3", - "bare-timers": "^3.0.1", - "bare-tls": "^2.0.4", - "bare-tty": "^5.0.2", - "bare-url": "^2.1.6", - "bare-utils": "^1.2.1", - "bare-vm": "^1.0.0", - "bare-worker": "^3.0.0", - "bare-zlib": "^1.2.5", - "buffer": "npm:bare-node-buffer@^1.0.0", - "child_process": "npm:bare-node-child-process", - "cluster": "npm:bare-node-cluster@^0.0.1", - "console": "npm:bare-node-console@^1.0.1", - "constants": "npm:bare-node-constants@^0.0.1", - "crypto": "npm:bare-node-crypto@^1.0.0", - "dgram": "npm:bare-node-dgram@^1.0.0", - "diagnostics_channel": "npm:bare-node-diagnostics-channel@^0.0.1", - "dns": "npm:bare-node-dns@^1.0.0", - "domain": "npm:bare-node-domain@^0.0.1", - "events": "npm:bare-node-events", - "fs": "npm:bare-node-fs", - "http": "npm:bare-node-http", - "http2": "npm:bare-node-http2@^0.0.1", - "https": "npm:bare-node-https@^1.0.0", - "inspector": "npm:bare-node-inspector@^1.0.1", - "module": "npm:bare-node-module@^1.0.0", - "net": "npm:bare-node-net@^1.0.0", - "os": "npm:bare-node-os@^1.0.1", - "path": "npm:bare-node-path", - "perf_hooks": "npm:bare-node-perf-hooks@^0.0.1", - "process": "npm:bare-node-process", - "punycode": "npm:bare-node-punycode@^0.0.1", - "querystring": "npm:bare-node-querystring@^1.0.0", - "readline": "npm:bare-node-readline@^1.0.1", - "repl": "npm:bare-node-repl@^1.0.1", - "sea": "npm:bare-node-sea@^0.0.0", - "sqlite": "npm:bare-node-sqlite@^0.0.0", - "stream": "npm:bare-node-stream@^1.0.0", - "string_decoder": "npm:bare-node-string-decoder@^0.0.1", - "sys": "npm:bare-node-sys@^0.0.1", - "test": "npm:bare-node-test@^0.0.0", - "timers": "npm:bare-node-timers@^1.0.0", - "tls": "npm:bare-node-tls@^1.0.0", - "tty": "npm:bare-node-tty@^1.0.1", - "url": "npm:bare-node-url@^1.0.1", - "util": "npm:bare-node-util@^1.0.0", - "v8": "npm:bare-node-v8@^0.0.1", - "vm": "npm:bare-node-vm@^1.0.0", - "wasi": "npm:bare-node-wasi@^0.0.1", - "worker_threads": "npm:bare-node-worker-threads@^1.0.0", - "zlib": "npm:bare-node-zlib@^1.0.0" - }, - "devDependencies": { - "brittle": "^3.0.0", - "pear-interface": "^1.0.0" - } -} diff --git a/init/templates/node-compat/test/index.test.js b/init/templates/node-compat/test/index.test.js deleted file mode 100644 index d9b912ddc..000000000 --- a/init/templates/node-compat/test/index.test.js +++ /dev/null @@ -1 +0,0 @@ -import test from 'brittle' // https://github.com/holepunchto/brittle