diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..0bd903c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/test.js" + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b81df5 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# deploy-to-neocities + + +## See also + +- [Neocities API Docs](https://neocities.org/api) +- [neocities/neocities-node](https://github.com/neocities/neocities-node): Node api diff --git a/dist/bundle.cjs.js b/dist/bundle.cjs.js deleted file mode 100644 index f234577..0000000 --- a/dist/bundle.cjs.js +++ /dev/null @@ -1,1678 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, '__esModule', { value: true }); - -function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } - -var Stream = _interopDefault(require('stream')); -var http = _interopDefault(require('http')); -var Url = require('url'); -var Url__default = _interopDefault(Url); -var https = _interopDefault(require('https')); -var zlib = _interopDefault(require('zlib')); -var qs = _interopDefault(require('querystring')); - -var nanoassert = assert; - -class AssertionError extends Error {} -AssertionError.prototype.name = 'AssertionError'; - -/** - * Minimal assert function - * @param {any} t Value to check if falsy - * @param {string=} m Optional assertion error message - * @throws {AssertionError} - */ -function assert (t, m) { - if (!t) { - var err = new AssertionError(m); - if (Error.captureStackTrace) Error.captureStackTrace(err, assert); - throw err - } -} - -// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js - -// fix for "Readable" isn't a named export issue -const Readable = Stream.Readable; - -const BUFFER = Symbol('buffer'); -const TYPE = Symbol('type'); - -class Blob { - constructor() { - this[TYPE] = ''; - - const blobParts = arguments[0]; - const options = arguments[1]; - - const buffers = []; - let size = 0; - - if (blobParts) { - const a = blobParts; - const length = Number(a.length); - for (let i = 0; i < length; i++) { - const element = a[i]; - let buffer; - if (element instanceof Buffer) { - buffer = element; - } else if (ArrayBuffer.isView(element)) { - buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength); - } else if (element instanceof ArrayBuffer) { - buffer = Buffer.from(element); - } else if (element instanceof Blob) { - buffer = element[BUFFER]; - } else { - buffer = Buffer.from(typeof element === 'string' ? element : String(element)); - } - size += buffer.length; - buffers.push(buffer); - } - } - - this[BUFFER] = Buffer.concat(buffers); - - let type = options && options.type !== undefined && String(options.type).toLowerCase(); - if (type && !/[^\u0020-\u007E]/.test(type)) { - this[TYPE] = type; - } - } - get size() { - return this[BUFFER].length; - } - get type() { - return this[TYPE]; - } - text() { - return Promise.resolve(this[BUFFER].toString()); - } - arrayBuffer() { - const buf = this[BUFFER]; - const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); - return Promise.resolve(ab); - } - stream() { - const readable = new Readable(); - readable._read = function () {}; - readable.push(this[BUFFER]); - readable.push(null); - return readable; - } - toString() { - return '[object Blob]'; - } - slice() { - const size = this.size; - - const start = arguments[0]; - const end = arguments[1]; - let relativeStart, relativeEnd; - if (start === undefined) { - relativeStart = 0; - } else if (start < 0) { - relativeStart = Math.max(size + start, 0); - } else { - relativeStart = Math.min(start, size); - } - if (end === undefined) { - relativeEnd = size; - } else if (end < 0) { - relativeEnd = Math.max(size + end, 0); - } else { - relativeEnd = Math.min(end, size); - } - const span = Math.max(relativeEnd - relativeStart, 0); - - const buffer = this[BUFFER]; - const slicedBuffer = buffer.slice(relativeStart, relativeStart + span); - const blob = new Blob([], { type: arguments[2] }); - blob[BUFFER] = slicedBuffer; - return blob; - } -} - -Object.defineProperties(Blob.prototype, { - size: { enumerable: true }, - type: { enumerable: true }, - slice: { enumerable: true } -}); - -Object.defineProperty(Blob.prototype, Symbol.toStringTag, { - value: 'Blob', - writable: false, - enumerable: false, - configurable: true -}); - -/** - * fetch-error.js - * - * FetchError interface for operational errors - */ - -/** - * Create FetchError instance - * - * @param String message Error message for human - * @param String type Error type for machine - * @param String systemError For Node.js system error - * @return FetchError - */ -function FetchError(message, type, systemError) { - Error.call(this, message); - - this.message = message; - this.type = type; - - // when err.type is `system`, err.code contains system error code - if (systemError) { - this.code = this.errno = systemError.code; - } - - // hide custom error implementation details from end-users - Error.captureStackTrace(this, this.constructor); -} - -FetchError.prototype = Object.create(Error.prototype); -FetchError.prototype.constructor = FetchError; -FetchError.prototype.name = 'FetchError'; - -let convert; -try { - convert = require('encoding').convert; -} catch (e) {} - -const INTERNALS = Symbol('Body internals'); - -// fix an issue where "PassThrough" isn't a named export for node <10 -const PassThrough = Stream.PassThrough; - -/** - * Body mixin - * - * Ref: https://fetch.spec.whatwg.org/#body - * - * @param Stream body Readable stream - * @param Object opts Response options - * @return Void - */ -function Body(body) { - var _this = this; - - var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, - _ref$size = _ref.size; - - let size = _ref$size === undefined ? 0 : _ref$size; - var _ref$timeout = _ref.timeout; - let timeout = _ref$timeout === undefined ? 0 : _ref$timeout; - - if (body == null) { - // body is undefined or null - body = null; - } else if (isURLSearchParams(body)) { - // body is a URLSearchParams - body = Buffer.from(body.toString()); - } else if (isBlob(body)) ; else if (Buffer.isBuffer(body)) ; else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') { - // body is ArrayBuffer - body = Buffer.from(body); - } else if (ArrayBuffer.isView(body)) { - // body is ArrayBufferView - body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); - } else if (body instanceof Stream) ; else { - // none of the above - // coerce to string then buffer - body = Buffer.from(String(body)); - } - this[INTERNALS] = { - body, - disturbed: false, - error: null - }; - this.size = size; - this.timeout = timeout; - - if (body instanceof Stream) { - body.on('error', function (err) { - const error = err.name === 'AbortError' ? err : new FetchError(`Invalid response body while trying to fetch ${_this.url}: ${err.message}`, 'system', err); - _this[INTERNALS].error = error; - }); - } -} - -Body.prototype = { - get body() { - return this[INTERNALS].body; - }, - - get bodyUsed() { - return this[INTERNALS].disturbed; - }, - - /** - * Decode response as ArrayBuffer - * - * @return Promise - */ - arrayBuffer() { - return consumeBody.call(this).then(function (buf) { - return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); - }); - }, - - /** - * Return raw response as Blob - * - * @return Promise - */ - blob() { - let ct = this.headers && this.headers.get('content-type') || ''; - return consumeBody.call(this).then(function (buf) { - return Object.assign( - // Prevent copying - new Blob([], { - type: ct.toLowerCase() - }), { - [BUFFER]: buf - }); - }); - }, - - /** - * Decode response as json - * - * @return Promise - */ - json() { - var _this2 = this; - - return consumeBody.call(this).then(function (buffer) { - try { - return JSON.parse(buffer.toString()); - } catch (err) { - return Body.Promise.reject(new FetchError(`invalid json response body at ${_this2.url} reason: ${err.message}`, 'invalid-json')); - } - }); - }, - - /** - * Decode response as text - * - * @return Promise - */ - text() { - return consumeBody.call(this).then(function (buffer) { - return buffer.toString(); - }); - }, - - /** - * Decode response as buffer (non-spec api) - * - * @return Promise - */ - buffer() { - return consumeBody.call(this); - }, - - /** - * Decode response as text, while automatically detecting the encoding and - * trying to decode to UTF-8 (non-spec api) - * - * @return Promise - */ - textConverted() { - var _this3 = this; - - return consumeBody.call(this).then(function (buffer) { - return convertBody(buffer, _this3.headers); - }); - } -}; - -// In browsers, all properties are enumerable. -Object.defineProperties(Body.prototype, { - body: { enumerable: true }, - bodyUsed: { enumerable: true }, - arrayBuffer: { enumerable: true }, - blob: { enumerable: true }, - json: { enumerable: true }, - text: { enumerable: true } -}); - -Body.mixIn = function (proto) { - for (const name of Object.getOwnPropertyNames(Body.prototype)) { - // istanbul ignore else: future proof - if (!(name in proto)) { - const desc = Object.getOwnPropertyDescriptor(Body.prototype, name); - Object.defineProperty(proto, name, desc); - } - } -}; - -/** - * Consume and convert an entire Body to a Buffer. - * - * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body - * - * @return Promise - */ -function consumeBody() { - var _this4 = this; - - if (this[INTERNALS].disturbed) { - return Body.Promise.reject(new TypeError(`body used already for: ${this.url}`)); - } - - this[INTERNALS].disturbed = true; - - if (this[INTERNALS].error) { - return Body.Promise.reject(this[INTERNALS].error); - } - - let body = this.body; - - // body is null - if (body === null) { - return Body.Promise.resolve(Buffer.alloc(0)); - } - - // body is blob - if (isBlob(body)) { - body = body.stream(); - } - - // body is buffer - if (Buffer.isBuffer(body)) { - return Body.Promise.resolve(body); - } - - // istanbul ignore if: should never happen - if (!(body instanceof Stream)) { - return Body.Promise.resolve(Buffer.alloc(0)); - } - - // body is stream - // get ready to actually consume the body - let accum = []; - let accumBytes = 0; - let abort = false; - - return new Body.Promise(function (resolve, reject) { - let resTimeout; - - // allow timeout on slow response body - if (_this4.timeout) { - resTimeout = setTimeout(function () { - abort = true; - reject(new FetchError(`Response timeout while trying to fetch ${_this4.url} (over ${_this4.timeout}ms)`, 'body-timeout')); - }, _this4.timeout); - } - - // handle stream errors - body.on('error', function (err) { - if (err.name === 'AbortError') { - // if the request was aborted, reject with this Error - abort = true; - reject(err); - } else { - // other errors, such as incorrect content-encoding - reject(new FetchError(`Invalid response body while trying to fetch ${_this4.url}: ${err.message}`, 'system', err)); - } - }); - - body.on('data', function (chunk) { - if (abort || chunk === null) { - return; - } - - if (_this4.size && accumBytes + chunk.length > _this4.size) { - abort = true; - reject(new FetchError(`content size at ${_this4.url} over limit: ${_this4.size}`, 'max-size')); - return; - } - - accumBytes += chunk.length; - accum.push(chunk); - }); - - body.on('end', function () { - if (abort) { - return; - } - - clearTimeout(resTimeout); - - try { - resolve(Buffer.concat(accum, accumBytes)); - } catch (err) { - // handle streams that have accumulated too much data (issue #414) - reject(new FetchError(`Could not create Buffer from response body for ${_this4.url}: ${err.message}`, 'system', err)); - } - }); - }); -} - -/** - * Detect buffer encoding and convert to target encoding - * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding - * - * @param Buffer buffer Incoming buffer - * @param String encoding Target encoding - * @return String - */ -function convertBody(buffer, headers) { - if (typeof convert !== 'function') { - throw new Error('The package `encoding` must be installed to use the textConverted() function'); - } - - const ct = headers.get('content-type'); - let charset = 'utf-8'; - let res, str; - - // header - if (ct) { - res = /charset=([^;]*)/i.exec(ct); - } - - // no charset in content type, peek at response body for at most 1024 bytes - str = buffer.slice(0, 1024).toString(); - - // html5 - if (!res && str) { - res = / 0 && arguments[0] !== undefined ? arguments[0] : undefined; - - this[MAP] = Object.create(null); - - if (init instanceof Headers) { - const rawHeaders = init.raw(); - const headerNames = Object.keys(rawHeaders); - - for (const headerName of headerNames) { - for (const value of rawHeaders[headerName]) { - this.append(headerName, value); - } - } - - return; - } - - // We don't worry about converting prop to ByteString here as append() - // will handle it. - if (init == null) ; else if (typeof init === 'object') { - const method = init[Symbol.iterator]; - if (method != null) { - if (typeof method !== 'function') { - throw new TypeError('Header pairs must be iterable'); - } - - // sequence> - // Note: per spec we have to first exhaust the lists then process them - const pairs = []; - for (const pair of init) { - if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') { - throw new TypeError('Each header pair must be iterable'); - } - pairs.push(Array.from(pair)); - } - - for (const pair of pairs) { - if (pair.length !== 2) { - throw new TypeError('Each header pair must be a name/value tuple'); - } - this.append(pair[0], pair[1]); - } - } else { - // record - for (const key of Object.keys(init)) { - const value = init[key]; - this.append(key, value); - } - } - } else { - throw new TypeError('Provided initializer must be an object'); - } - } - - /** - * Return combined header value given name - * - * @param String name Header name - * @return Mixed - */ - get(name) { - name = `${name}`; - validateName(name); - const key = find(this[MAP], name); - if (key === undefined) { - return null; - } - - return this[MAP][key].join(', '); - } - - /** - * Iterate over all headers - * - * @param Function callback Executed for each item with parameters (value, name, thisArg) - * @param Boolean thisArg `this` context for callback function - * @return Void - */ - forEach(callback) { - let thisArg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; - - let pairs = getHeaders(this); - let i = 0; - while (i < pairs.length) { - var _pairs$i = pairs[i]; - const name = _pairs$i[0], - value = _pairs$i[1]; - - callback.call(thisArg, value, name, this); - pairs = getHeaders(this); - i++; - } - } - - /** - * Overwrite header values given name - * - * @param String name Header name - * @param String value Header value - * @return Void - */ - set(name, value) { - name = `${name}`; - value = `${value}`; - validateName(name); - validateValue(value); - const key = find(this[MAP], name); - this[MAP][key !== undefined ? key : name] = [value]; - } - - /** - * Append a value onto existing header - * - * @param String name Header name - * @param String value Header value - * @return Void - */ - append(name, value) { - name = `${name}`; - value = `${value}`; - validateName(name); - validateValue(value); - const key = find(this[MAP], name); - if (key !== undefined) { - this[MAP][key].push(value); - } else { - this[MAP][name] = [value]; - } - } - - /** - * Check for header name existence - * - * @param String name Header name - * @return Boolean - */ - has(name) { - name = `${name}`; - validateName(name); - return find(this[MAP], name) !== undefined; - } - - /** - * Delete all header values given name - * - * @param String name Header name - * @return Void - */ - delete(name) { - name = `${name}`; - validateName(name); - const key = find(this[MAP], name); - if (key !== undefined) { - delete this[MAP][key]; - } - } - - /** - * Return raw headers (non-spec api) - * - * @return Object - */ - raw() { - return this[MAP]; - } - - /** - * Get an iterator on keys. - * - * @return Iterator - */ - keys() { - return createHeadersIterator(this, 'key'); - } - - /** - * Get an iterator on values. - * - * @return Iterator - */ - values() { - return createHeadersIterator(this, 'value'); - } - - /** - * Get an iterator on entries. - * - * This is the default iterator of the Headers object. - * - * @return Iterator - */ - [Symbol.iterator]() { - return createHeadersIterator(this, 'key+value'); - } -} -Headers.prototype.entries = Headers.prototype[Symbol.iterator]; - -Object.defineProperty(Headers.prototype, Symbol.toStringTag, { - value: 'Headers', - writable: false, - enumerable: false, - configurable: true -}); - -Object.defineProperties(Headers.prototype, { - get: { enumerable: true }, - forEach: { enumerable: true }, - set: { enumerable: true }, - append: { enumerable: true }, - has: { enumerable: true }, - delete: { enumerable: true }, - keys: { enumerable: true }, - values: { enumerable: true }, - entries: { enumerable: true } -}); - -function getHeaders(headers) { - let kind = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'key+value'; - - const keys = Object.keys(headers[MAP]).sort(); - return keys.map(kind === 'key' ? function (k) { - return k.toLowerCase(); - } : kind === 'value' ? function (k) { - return headers[MAP][k].join(', '); - } : function (k) { - return [k.toLowerCase(), headers[MAP][k].join(', ')]; - }); -} - -const INTERNAL = Symbol('internal'); - -function createHeadersIterator(target, kind) { - const iterator = Object.create(HeadersIteratorPrototype); - iterator[INTERNAL] = { - target, - kind, - index: 0 - }; - return iterator; -} - -const HeadersIteratorPrototype = Object.setPrototypeOf({ - next() { - // istanbul ignore if - if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { - throw new TypeError('Value of `this` is not a HeadersIterator'); - } - - var _INTERNAL = this[INTERNAL]; - const target = _INTERNAL.target, - kind = _INTERNAL.kind, - index = _INTERNAL.index; - - const values = getHeaders(target, kind); - const len = values.length; - if (index >= len) { - return { - value: undefined, - done: true - }; - } - - this[INTERNAL].index = index + 1; - - return { - value: values[index], - done: false - }; - } -}, Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))); - -Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { - value: 'HeadersIterator', - writable: false, - enumerable: false, - configurable: true -}); - -/** - * Export the Headers object in a form that Node.js can consume. - * - * @param Headers headers - * @return Object - */ -function exportNodeCompatibleHeaders(headers) { - const obj = Object.assign({ __proto__: null }, headers[MAP]); - - // http.request() only supports string as Host header. This hack makes - // specifying custom Host header possible. - const hostHeaderKey = find(headers[MAP], 'Host'); - if (hostHeaderKey !== undefined) { - obj[hostHeaderKey] = obj[hostHeaderKey][0]; - } - - return obj; -} - -/** - * Create a Headers object from an object of headers, ignoring those that do - * not conform to HTTP grammar productions. - * - * @param Object obj Object of headers - * @return Headers - */ -function createHeadersLenient(obj) { - const headers = new Headers(); - for (const name of Object.keys(obj)) { - if (invalidTokenRegex.test(name)) { - continue; - } - if (Array.isArray(obj[name])) { - for (const val of obj[name]) { - if (invalidHeaderCharRegex.test(val)) { - continue; - } - if (headers[MAP][name] === undefined) { - headers[MAP][name] = [val]; - } else { - headers[MAP][name].push(val); - } - } - } else if (!invalidHeaderCharRegex.test(obj[name])) { - headers[MAP][name] = [obj[name]]; - } - } - return headers; -} - -const INTERNALS$1 = Symbol('Response internals'); - -// fix an issue where "STATUS_CODES" aren't a named export for node <10 -const STATUS_CODES = http.STATUS_CODES; - -/** - * Response class - * - * @param Stream body Readable stream - * @param Object opts Response options - * @return Void - */ -class Response { - constructor() { - let body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - let opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - - Body.call(this, body, opts); - - const status = opts.status || 200; - const headers = new Headers(opts.headers); - - if (body != null && !headers.has('Content-Type')) { - const contentType = extractContentType(body); - if (contentType) { - headers.append('Content-Type', contentType); - } - } - - this[INTERNALS$1] = { - url: opts.url, - status, - statusText: opts.statusText || STATUS_CODES[status], - headers, - counter: opts.counter - }; - } - - get url() { - return this[INTERNALS$1].url || ''; - } - - get status() { - return this[INTERNALS$1].status; - } - - /** - * Convenience property representing if the request ended normally - */ - get ok() { - return this[INTERNALS$1].status >= 200 && this[INTERNALS$1].status < 300; - } - - get redirected() { - return this[INTERNALS$1].counter > 0; - } - - get statusText() { - return this[INTERNALS$1].statusText; - } - - get headers() { - return this[INTERNALS$1].headers; - } - - /** - * Clone this response - * - * @return Response - */ - clone() { - return new Response(clone(this), { - url: this.url, - status: this.status, - statusText: this.statusText, - headers: this.headers, - ok: this.ok, - redirected: this.redirected - }); - } -} - -Body.mixIn(Response.prototype); - -Object.defineProperties(Response.prototype, { - url: { enumerable: true }, - status: { enumerable: true }, - ok: { enumerable: true }, - redirected: { enumerable: true }, - statusText: { enumerable: true }, - headers: { enumerable: true }, - clone: { enumerable: true } -}); - -Object.defineProperty(Response.prototype, Symbol.toStringTag, { - value: 'Response', - writable: false, - enumerable: false, - configurable: true -}); - -const INTERNALS$2 = Symbol('Request internals'); - -// fix an issue where "format", "parse" aren't a named export for node <10 -const parse_url = Url__default.parse; -const format_url = Url__default.format; - -const streamDestructionSupported = 'destroy' in Stream.Readable.prototype; - -/** - * Check if a value is an instance of Request. - * - * @param Mixed input - * @return Boolean - */ -function isRequest(input) { - return typeof input === 'object' && typeof input[INTERNALS$2] === 'object'; -} - -function isAbortSignal(signal) { - const proto = signal && typeof signal === 'object' && Object.getPrototypeOf(signal); - return !!(proto && proto.constructor.name === 'AbortSignal'); -} - -/** - * Request class - * - * @param Mixed input Url or Request instance - * @param Object init Custom options - * @return Void - */ -class Request { - constructor(input) { - let init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - - let parsedURL; - - // normalize input - if (!isRequest(input)) { - if (input && input.href) { - // in order to support Node.js' Url objects; though WHATWG's URL objects - // will fall into this branch also (since their `toString()` will return - // `href` property anyway) - parsedURL = parse_url(input.href); - } else { - // coerce input to a string before attempting to parse - parsedURL = parse_url(`${input}`); - } - input = {}; - } else { - parsedURL = parse_url(input.url); - } - - let method = init.method || input.method || 'GET'; - method = method.toUpperCase(); - - if ((init.body != null || isRequest(input) && input.body !== null) && (method === 'GET' || method === 'HEAD')) { - throw new TypeError('Request with GET/HEAD method cannot have body'); - } - - let inputBody = init.body != null ? init.body : isRequest(input) && input.body !== null ? clone(input) : null; - - Body.call(this, inputBody, { - timeout: init.timeout || input.timeout || 0, - size: init.size || input.size || 0 - }); - - const headers = new Headers(init.headers || input.headers || {}); - - if (inputBody != null && !headers.has('Content-Type')) { - const contentType = extractContentType(inputBody); - if (contentType) { - headers.append('Content-Type', contentType); - } - } - - let signal = isRequest(input) ? input.signal : null; - if ('signal' in init) signal = init.signal; - - if (signal != null && !isAbortSignal(signal)) { - throw new TypeError('Expected signal to be an instanceof AbortSignal'); - } - - this[INTERNALS$2] = { - method, - redirect: init.redirect || input.redirect || 'follow', - headers, - parsedURL, - signal - }; - - // node-fetch-only options - this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? input.follow : 20; - this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? input.compress : true; - this.counter = init.counter || input.counter || 0; - this.agent = init.agent || input.agent; - } - - get method() { - return this[INTERNALS$2].method; - } - - get url() { - return format_url(this[INTERNALS$2].parsedURL); - } - - get headers() { - return this[INTERNALS$2].headers; - } - - get redirect() { - return this[INTERNALS$2].redirect; - } - - get signal() { - return this[INTERNALS$2].signal; - } - - /** - * Clone this request - * - * @return Request - */ - clone() { - return new Request(this); - } -} - -Body.mixIn(Request.prototype); - -Object.defineProperty(Request.prototype, Symbol.toStringTag, { - value: 'Request', - writable: false, - enumerable: false, - configurable: true -}); - -Object.defineProperties(Request.prototype, { - method: { enumerable: true }, - url: { enumerable: true }, - headers: { enumerable: true }, - redirect: { enumerable: true }, - clone: { enumerable: true }, - signal: { enumerable: true } -}); - -/** - * Convert a Request to Node.js http request options. - * - * @param Request A Request instance - * @return Object The options object to be passed to http.request - */ -function getNodeRequestOptions(request) { - const parsedURL = request[INTERNALS$2].parsedURL; - const headers = new Headers(request[INTERNALS$2].headers); - - // fetch step 1.3 - if (!headers.has('Accept')) { - headers.set('Accept', '*/*'); - } - - // Basic fetch - if (!parsedURL.protocol || !parsedURL.hostname) { - throw new TypeError('Only absolute URLs are supported'); - } - - if (!/^https?:$/.test(parsedURL.protocol)) { - throw new TypeError('Only HTTP(S) protocols are supported'); - } - - if (request.signal && request.body instanceof Stream.Readable && !streamDestructionSupported) { - throw new Error('Cancellation of streamed requests with AbortSignal is not supported in node < 8'); - } - - // HTTP-network-or-cache fetch steps 2.4-2.7 - let contentLengthValue = null; - if (request.body == null && /^(POST|PUT)$/i.test(request.method)) { - contentLengthValue = '0'; - } - if (request.body != null) { - const totalBytes = getTotalBytes(request); - if (typeof totalBytes === 'number') { - contentLengthValue = String(totalBytes); - } - } - if (contentLengthValue) { - headers.set('Content-Length', contentLengthValue); - } - - // HTTP-network-or-cache fetch step 2.11 - if (!headers.has('User-Agent')) { - headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)'); - } - - // HTTP-network-or-cache fetch step 2.15 - if (request.compress && !headers.has('Accept-Encoding')) { - headers.set('Accept-Encoding', 'gzip,deflate'); - } - - let agent = request.agent; - if (typeof agent === 'function') { - agent = agent(parsedURL); - } - - if (!headers.has('Connection') && !agent) { - headers.set('Connection', 'close'); - } - - // HTTP-network fetch step 4.2 - // chunked encoding is handled by Node.js - - return Object.assign({}, parsedURL, { - method: request.method, - headers: exportNodeCompatibleHeaders(headers), - agent - }); -} - -/** - * abort-error.js - * - * AbortError interface for cancelled requests - */ - -/** - * Create AbortError instance - * - * @param String message Error message for human - * @return AbortError - */ -function AbortError(message) { - Error.call(this, message); - - this.type = 'aborted'; - this.message = message; - - // hide custom error implementation details from end-users - Error.captureStackTrace(this, this.constructor); -} - -AbortError.prototype = Object.create(Error.prototype); -AbortError.prototype.constructor = AbortError; -AbortError.prototype.name = 'AbortError'; - -// fix an issue where "PassThrough", "resolve" aren't a named export for node <10 -const PassThrough$1 = Stream.PassThrough; -const resolve_url = Url__default.resolve; - -/** - * Fetch function - * - * @param Mixed url Absolute url or Request instance - * @param Object opts Fetch options - * @return Promise - */ -function fetch(url, opts) { - - // allow custom promise - if (!fetch.Promise) { - throw new Error('native promise missing, set fetch.Promise to your favorite alternative'); - } - - Body.Promise = fetch.Promise; - - // wrap http.request into fetch - return new fetch.Promise(function (resolve, reject) { - // build request object - const request = new Request(url, opts); - const options = getNodeRequestOptions(request); - - const send = (options.protocol === 'https:' ? https : http).request; - const signal = request.signal; - - let response = null; - - const abort = function abort() { - let error = new AbortError('The user aborted a request.'); - reject(error); - if (request.body && request.body instanceof Stream.Readable) { - request.body.destroy(error); - } - if (!response || !response.body) return; - response.body.emit('error', error); - }; - - if (signal && signal.aborted) { - abort(); - return; - } - - const abortAndFinalize = function abortAndFinalize() { - abort(); - finalize(); - }; - - // send request - const req = send(options); - let reqTimeout; - - if (signal) { - signal.addEventListener('abort', abortAndFinalize); - } - - function finalize() { - req.abort(); - if (signal) signal.removeEventListener('abort', abortAndFinalize); - clearTimeout(reqTimeout); - } - - if (request.timeout) { - req.once('socket', function (socket) { - reqTimeout = setTimeout(function () { - reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); - finalize(); - }, request.timeout); - }); - } - - req.on('error', function (err) { - reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); - finalize(); - }); - - req.on('response', function (res) { - clearTimeout(reqTimeout); - - const headers = createHeadersLenient(res.headers); - - // HTTP fetch step 5 - if (fetch.isRedirect(res.statusCode)) { - // HTTP fetch step 5.2 - const location = headers.get('Location'); - - // HTTP fetch step 5.3 - const locationURL = location === null ? null : resolve_url(request.url, location); - - // HTTP fetch step 5.5 - switch (request.redirect) { - case 'error': - reject(new FetchError(`redirect mode is set to error: ${request.url}`, 'no-redirect')); - finalize(); - return; - case 'manual': - // node-fetch-specific step: make manual redirect a bit easier to use by setting the Location header value to the resolved URL. - if (locationURL !== null) { - // handle corrupted header - try { - headers.set('Location', locationURL); - } catch (err) { - // istanbul ignore next: nodejs server prevent invalid response headers, we can't test this through normal request - reject(err); - } - } - break; - case 'follow': - // HTTP-redirect fetch step 2 - if (locationURL === null) { - break; - } - - // HTTP-redirect fetch step 5 - if (request.counter >= request.follow) { - reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect')); - finalize(); - return; - } - - // HTTP-redirect fetch step 6 (counter increment) - // Create a new Request object. - const requestOpts = { - headers: new Headers(request.headers), - follow: request.follow, - counter: request.counter + 1, - agent: request.agent, - compress: request.compress, - method: request.method, - body: request.body, - signal: request.signal, - timeout: request.timeout - }; - - // HTTP-redirect fetch step 9 - if (res.statusCode !== 303 && request.body && getTotalBytes(request) === null) { - reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect')); - finalize(); - return; - } - - // HTTP-redirect fetch step 11 - if (res.statusCode === 303 || (res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST') { - requestOpts.method = 'GET'; - requestOpts.body = undefined; - requestOpts.headers.delete('content-length'); - } - - // HTTP-redirect fetch step 15 - resolve(fetch(new Request(locationURL, requestOpts))); - finalize(); - return; - } - } - - // prepare response - res.once('end', function () { - if (signal) signal.removeEventListener('abort', abortAndFinalize); - }); - let body = res.pipe(new PassThrough$1()); - - const response_options = { - url: request.url, - status: res.statusCode, - statusText: res.statusMessage, - headers: headers, - size: request.size, - timeout: request.timeout, - counter: request.counter - }; - - // HTTP-network fetch step 12.1.1.3 - const codings = headers.get('Content-Encoding'); - - // HTTP-network fetch step 12.1.1.4: handle content codings - - // in following scenarios we ignore compression support - // 1. compression support is disabled - // 2. HEAD request - // 3. no Content-Encoding header - // 4. no content response (204) - // 5. content not modified response (304) - if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) { - response = new Response(body, response_options); - resolve(response); - return; - } - - // For Node v6+ - // Be less strict when decoding compressed responses, since sometimes - // servers send slightly invalid responses that are still accepted - // by common browsers. - // Always using Z_SYNC_FLUSH is what cURL does. - const zlibOptions = { - flush: zlib.Z_SYNC_FLUSH, - finishFlush: zlib.Z_SYNC_FLUSH - }; - - // for gzip - if (codings == 'gzip' || codings == 'x-gzip') { - body = body.pipe(zlib.createGunzip(zlibOptions)); - response = new Response(body, response_options); - resolve(response); - return; - } - - // for deflate - if (codings == 'deflate' || codings == 'x-deflate') { - // handle the infamous raw deflate response from old servers - // a hack for old IIS and Apache servers - const raw = res.pipe(new PassThrough$1()); - raw.once('data', function (chunk) { - // see http://stackoverflow.com/questions/37519828 - if ((chunk[0] & 0x0F) === 0x08) { - body = body.pipe(zlib.createInflate()); - } else { - body = body.pipe(zlib.createInflateRaw()); - } - response = new Response(body, response_options); - resolve(response); - }); - return; - } - - // for br - if (codings == 'br' && typeof zlib.createBrotliDecompress === 'function') { - body = body.pipe(zlib.createBrotliDecompress()); - response = new Response(body, response_options); - resolve(response); - return; - } - - // otherwise, use response as-is - response = new Response(body, response_options); - resolve(response); - }); - - writeToStream(req, request); - }); -} -/** - * Redirect code matching - * - * @param Number code Status code - * @return Boolean - */ -fetch.isRedirect = function (code) { - return code === 301 || code === 302 || code === 303 || code === 307 || code === 308; -}; - -// expose Promise -fetch.Promise = global.Promise; - -class NeocitiesAPIClient { - constructor (apiKey, opts) { - nanoassert(apiKey, 'must pass apiKey as first argument'); - nanoassert(typeof apiKey === 'string', 'apiKey must be a string'); - opts = Object.assign({ - url: 'https://neocities.org' - }); - - this.opts = opts; - this.url = new Url.URL(opts.url); - } - - get (method, args) { - nanoassert(method, 'must pass method as first argument'); - let path = `/api/${method}`; - if (args) path += `?${qs.stringify(args)}`; - - return fetch(path); - } -} - -exports.NeocitiesAPIClient = NeocitiesAPIClient; diff --git a/fixtures/toot.gif b/fixtures/toot.gif new file mode 100755 index 0000000..b75a98d Binary files /dev/null and b/fixtures/toot.gif differ diff --git a/fixtures/tootzzz.png b/fixtures/tootzzz.png new file mode 100755 index 0000000..171bd72 Binary files /dev/null and b/fixtures/tootzzz.png differ diff --git a/index.js b/index.js index 1cb0d29..918a5fc 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -// Set options as a parameter, environment variable, or rc file. -// eslint-disable-next-line no-global-assign -require = require('esm')(module/* , options */); -module.exports = require('./main.js'); + +const { NeocitiesAPIClient } = require('./lib/client.js') + +module.exports = { NeocitiesAPIClient } diff --git a/lib/client.js b/lib/client.js index 5d887a8..8ff003e 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,134 +1,124 @@ -import assert from 'nanoassert'; -import fetch from 'node-fetch'; -import { URL } from 'url'; -import qs from 'qs'; -import pkg from '../package.json'; -import os from 'os'; +const assert = require('nanoassert') +const fetch = require('node-fetch') +const { URL } = require('url') +const qs = require('qs') +const os = require('os') +const { createReadStream } = require('fs') +const FormData = require('form-data') +const { handleResponse } = require('fetch-errors') +const pkg = require('../package.json') -const defaultURL = 'https://neocities.org/api'; +const defaultURL = 'https://neocities.org' -export class NeocitiesAPIClient { +class NeocitiesAPIClient { static getKey (sitename, password, opts) { - assert(sitename, 'must pass sitename as first arg'); - assert(typeof sitename === 'string', 'user arg must be a string'); - assert(password, 'must pass a password as the second arg'); - assert(typeof password, 'password arg must be a string'); + assert(sitename, 'must pass sitename as first arg') + assert(typeof sitename === 'string', 'user arg must be a string') + assert(password, 'must pass a password as the second arg') + assert(typeof password, 'password arg must be a string') opts = Object.assign({ url: defaultURL - }, opts); + }, opts) - return fetch(); + return fetch() } constructor (apiKey, opts) { - assert(apiKey, 'must pass apiKey as first argument'); - assert(typeof apiKey === 'string', 'apiKey must be a string'); + assert(apiKey, 'must pass apiKey as first argument') + assert(typeof apiKey === 'string', 'apiKey must be a string') opts = Object.assign({ url: defaultURL - }); + }) - this.opts = opts; - this.url = opts.url; - this.apiKey = apiKey; + this.opts = opts + this.url = opts.url + this.apiKey = apiKey } - request (endpoint, args, opts) { - assert(endpoint, 'must pass endpoint as first argument'); - opts = Object.assign({}, opts); - opts.headers = Object.assign({ + get defaultHeaders () { + return { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'User-Agent': `deploy-to-neocities/${pkg.version} (${os.type()})` - }, opts.headers); - - let path = `/api/${endpoint}`; - if (args) path += `?${qs.stringify(args)}`; - - const url = new URL(path, this.url); - return fetch(url, opts); + } } - get (endpoint, args, opts) { + get (endpoint, quieries, opts) { + assert(endpoint, 'must pass endpoint as first argument') opts = Object.assign({ method: 'GET' - }, opts); - return this.request(endpoint, args, opts); + }, opts) + opts.headers = Object.assign({}, this.defaultHeaders, opts.headers) + + let path = `/api/${endpoint}` + if (quieries) path += `?${qs.stringify(quieries)}` + + const url = new URL(path, this.url) + return fetch(url, opts) } - post (endpoint, args, opts) { + post (endpoint, formEntries, opts) { + assert(endpoint, 'must pass endpoint as first argument') + const form = new FormData() opts = Object.assign({ - method: 'POST' - }, opts); - return this.request(endpoint, args, opts); + method: 'POST', + body: form + }, opts) + + for (const { name, value } of formEntries) { + form.append(name, value) + } + + opts.headers = Object.assign( + {}, + this.defaultHeaders, + form.getHeaders(), + opts.headers) + + const url = new URL(`/api/${endpoint}`, this.url) + return fetch(url, opts) } upload (files) { - throw new Error('NOT IMPLEMENTED'); - } + const formEntries = files.map(({ name, path }) => { + return { + name, + value: createReadStream(path) + } + }) - deploy (folder) { - throw new Error('NOT IMPLEMENTED'); + return this.post('upload', formEntries).then(handleResponse) } delete (filenames) { - const args = { - filenames - }; + assert(filenames, 'filenames is a required first argument') + assert(Array.isArray(filenames), 'filenames argument must be an array of file paths in your website') - return this.post('/delete', args); + const formEntries = filenames.map(file => ({ + name: 'filenames[]', + value: file + })) + + return this.post('delete', formEntries).then(handleResponse) } - list (args) { + list (queries) { // args.path: Path to list - return this.get('/list', args).then(handleResponse); + return this.get('list', queries).then(handleResponse) } /** * @param {Object} args Querystring arguments to include * @return {Promise} Fetch request promise */ - info (args) { + info (queries) { // args.sitename: sitename to get info on - return this.get('/info', args).then(handleResponse); + return this.get('info', queries).then(handleResponse) } -} -export async function handleResponse (response) { - const contentType = response.headers.get('Content-Type'); - const isJSON = contentType && contentType.match(/json/); - const data = isJSON ? await response.json() : await response.text(); - if (response.ok) return data; - else { - return isJSON - ? Promise.reject(new JSONHTTPError(response, data)) - : Promise.reject(new TextHTTPError(response, data)); - } -} - -export class HTTPError extends Error { - constructor (response) { - super(response.statusText); - this.name = this.constructor.name; - if (typeof Error.captureStackTrace === 'function') { - Error.captureStackTrace(this, this.constructor); - } else { - this.stack = new Error(response.statusText).stack; - } - this.status = response.status; - } -} - -export class TextHTTPError extends HTTPError { - constructor (response, data) { - super(response); - this.data = data; - } -} - -export class JSONHTTPError extends HTTPError { - constructor (response, json) { - super(response); - this.json = json; + deploy (folder) { + throw new Error('NOT IMPLEMENTED') } } +module.exports = { NeocitiesAPIClient } diff --git a/main.js b/main.js deleted file mode 100644 index b1adcfa..0000000 --- a/main.js +++ /dev/null @@ -1,4 +0,0 @@ -// ESM syntax is supported. -import { NeocitiesAPIClient } from './lib/client.js'; - -export { NeocitiesAPIClient }; diff --git a/package.json b/package.json index 7944fe2..261f0b6 100644 --- a/package.json +++ b/package.json @@ -3,17 +3,13 @@ "version": "0.0.0", "description": "Github Action to deplpoy a folder to Neocities.org", "main": "index.js", - "module": "main.js", "private": true, "scripts": { "test": "run-s test:*", "test-skip:deps": "dependency-check . --no-dev --no-peer", - "test:standard": "semistandard", - "test:tape": "tape -r esm test.js", - "prepare": "run-s build", - "clean": "rimraf dist && mkdirp dist", - "build": "run-s clean build:*", - "build:browserify": "rollup --config" + "test:standard": "standard", + "test:tape": "tap", + "clean": "rimraf dist && mkdirp dist" }, "repository": { "type": "git", @@ -29,24 +25,21 @@ "devDependencies": { "builtin-modules": "^3.1.0", "dependency-check": "^4.1.0", - "esm": "^3.2.25", "mkdirp": "^0.5.1", "npm-run-all": "^4.1.5", "rimraf": "^3.0.0", - "rollup": "^1.26.5", - "rollup-plugin-commonjs": "^10.1.0", - "rollup-plugin-node-resolve": "^5.2.0", - "semistandard": "^14.2.0", - "tape": "^4.11.0", - "tape-promise": "^4.0.0" + "standard": "^14.3.1", + "tap": "^14.10.5" }, "dependencies": { "@actions/core": "^1.2.0", "@actions/github": "^1.1.0", + "fetch-errors": "^1.0.1", + "form-data": "^3.0.0", "nanoassert": "^2.0.0", "qs": "^6.9.1" }, - "semistandard": { + "standard": { "ignore": [ "dist" ] diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index cae7ac4..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,21 +0,0 @@ - -import resolve from 'rollup-plugin-node-resolve'; -import commonjs from 'rollup-plugin-commonjs'; -import builtins from 'builtin-modules'; - -export default [ - { - input: 'main.js', - output: [ - { file: 'dist/bundle.cjs.js', format: 'cjs' } - ], - plugins: [ - resolve({ - browser: false, - preferBuiltins: true - }), - commonjs() - ], - external: builtins - } -]; diff --git a/test.js b/test.js index 9856f4a..279e01d 100644 --- a/test.js +++ b/test.js @@ -1,44 +1,82 @@ -import tape from 'tape'; -import ptape from 'tape-promise'; -import { readFileSync } from 'fs'; -import { resolve } from 'path'; -import { NeocitiesAPIClient } from './lib/client.js'; -const test = ptape(tape); +const tap = require('tap') -let token = process.env.NEOCITIES_API_TOKEN; +const { readFileSync } = require('fs') +const { resolve } = require('path') +const { NeocitiesAPIClient } = require('./lib/client') + +let token = process.env.NEOCITIES_API_TOKEN if (!token) { try { - const config = JSON.parse(readFileSync(resolve(__dirname, 'config.json'))); - token = config.token; + const config = JSON.parse(readFileSync(resolve(__dirname, 'config.json'))) + token = config.token } catch (e) { - console.warn('error loading config.json'); - console.warn(e); + console.warn('error loading config.json') + console.warn(e) } } -if (token) { - test('token loaded', async t => { - t.ok(token); - }); +tap.test('token loaded', async t => { + t.ok(token) +}) - test('basic client api', async t => { - const client = new NeocitiesAPIClient(token); +tap.test('basic client api', async t => { + const client = new NeocitiesAPIClient(token) - t.ok(client.info, 'info method available'); - t.ok(client.list, 'list method available'); - t.ok(client.get, 'get method available'); - t.ok(client.post, 'post method available'); - }); + t.ok(client.info, 'info method available') + t.ok(client.list, 'list method available') + t.ok(client.get, 'get method available') + t.ok(client.post, 'post method available') +}) - test('can get info about site', async t => { - const client = new NeocitiesAPIClient(token); +tap.test('can get info about site', async t => { + const client = new NeocitiesAPIClient(token) - const info = await client.info(); - console.log(info); - const list = await client.list(); - console.log(list); - }); -} else { - console.warn('No token set, live tests disabled'); -} + const info = await client.info() + console.log(info) + t.equal(info.result, 'success', 'info requesst successfull') + const list = await client.list() + console.log(list) + t.equal(list.result, 'success', 'list result successfull') +}) + +// test('form data works the way I think', t => { +// const form = new FormData(); +// const p = resolve(__dirname, 'package.json'); +// form.append('package.json', next => next(createReadStream(p))); +// +// const concatStream = concat((data) => { +// console.log(data); +// t.end(); +// }); +// +// form.on('error', (err) => { +// t.error(err); +// }); +// form.pipe(concatStream); +// }); + +tap.test('can upload and delete files', async t => { + const client = new NeocitiesAPIClient(token) + + const uploadResults = await client.upload([ + { + name: 'toot.gif', + path: resolve(__dirname, 'fixtures/toot.gif') + }, + { + name: 'img/tootzzz.png', + path: resolve(__dirname, 'fixtures/tootzzz.png') + } + ]) + + console.log(uploadResults) + t.equal(uploadResults.result, 'success', 'list result successfull') + + const deleteResults = await client.delete([ + 'toot.gif', + 'img/tootzzz.png' + ]) + console.log(deleteResults) + t.equal(deleteResults.result, 'success', 'list result successfull') +})