diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f7bfb37 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +# [5.0.0] + +### Changed + +- No more callbacks in JavaScript API. Asynchronous generators are used instead. +- The pipeline has been rewritten. Instead of interval-based approach, packages are now pushed after a delay since the last change. +- The `-i` (interval) argument becomes `-d` (delay). +- The `-d` (debug) argument becomes `-v` (verbose). +- The `-u` argument becomes `-q`. \ No newline at end of file diff --git a/README.md b/README.md index f1ea48a..695e3f3 100644 --- a/README.md +++ b/README.md @@ -25,28 +25,28 @@ Simply run `aemsync` on your project path, make a change to any of your files or ### Advanced usage -Commandline +CLI ``` Usage: aemsync [OPTIONS] Options: -t URL to AEM instance; multiple can be set. - Default: ${defaults.targets} + Default: http://admin:admin@localhost:4502 -w Watch over folder. - Default: CWD + Default: . -p Push specific file or folder. -e Extended glob filter; multiple can be set. Default: **/jcr_root/* **/@(.git|.svn|.hg|target) **/@(.git|.svn|.hg|target)/** - -i Update interval. - Default: ${defaults.interval} ms - -u Package manager path. - Default: ${defaults.packmgrPath} + -d Time to wait since the last change before push. + Default: undefined ms + -q Package manager path. + Default: /crx/packmgr/service.jsp -c Check if AEM is up and running before pushing. - -d Enable debug mode. + -v Enable verbose mode. -h Display this screen. Examples: @@ -58,85 +58,62 @@ Examples: > aemsync -e **/*.orig -e **/test -e -e **/test/** Just push, don't watch: > aemsync -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component + +Website: + https://github.com/gavoja/aemsync ``` -JavaScript (full watch example): +JavaScript API ```JavaScript -const aemsync = require('aemsync') - -const workingDir = '~/workspace/my_project' - -// Arguments below are optional. -const targets = [ - 'http://admin:admin@localhost:4502', - 'http://admin:admin@localhost:4503' -] -const exclude = ['**/*.orig'] // Skip merge files. -const packmgrPath = '/foo/crx/packmgr/service.jsp' -const interval = 300 -const onPushEnd = (err, target, log) => { - // Called for each of the targets. - if (err) { - console.log(`Error when pushing package to ${target}.`, err.message) - } else { - console.log(`Package pushed to ${target}. Response log:\n${log}`) - } -} -const checkBeforePush = true +import { aemsync, push } from 'aemsync' -// Will watch for changes over workingDir and push upon a file change. -// Only the first argument is mandatory. -aemsync(workingDir, { targets, exclude, interval, packmgrPath, onPushEnd, checkBeforePush }) +// Interactive watch example. +(async () => { + const args = { workingDir } + + for await (const result of aemsync(args)) { + console.log(result) + } +})() + +// Push example. +(async () => { + const args = { payload: [ + './foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component', + './foo/bar/my-workspace/jcr_content/apps/my-app/components/something-else' + ]} + + for await (const result of aemsync(args)) { + // Will yield one result for each target. + console.log(result) + } +})() ``` -JavaScript (direct push example): +JavaScript arguments and defaults for `aemsync()` and `push()` functions: ```JavaScript -const { push } = require('aemsync') - -const pathToPush = '~/foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component' - -// Arguments below are optional. -const targets = [ - 'http://admin:admin@localhost:4502', - 'http://admin:admin@localhost:4503' -] -const onPushEnd = (err, target, log) => { - // Called for each of the targets. - if (err) { - console.log(`Error when pushing package to ${target}.`, err.message) - } else { - console.log(`Package pushed to ${target}. Response log:\n${log}`) - } +const args = { + workingDir: '.', + exclude: ['**/jcr_root/*', '**/@(.git|.svn|.hg|target)', '**/@(.git|.svn|.hg|target)/**'], + packmgrPath: '/crx/packmgr/service.jsp', + targets: ['http://admin:admin@localhost:4501'], + delay: 200, + checkIfUp: false } -const checkBeforePush = true - -// Will push the path to AEM. -// To use await, the call must be made inside an async function. -// The result is a Promise so it can also be resolved with .then(). -// Only the first argument is mandatory. -await push(pathToPush, { targets, onPushEnd, checkBeforePush }) ``` ### Description -The Watcher uses Node's `fs.watch()` function to watch over directory changes recursively. For Windows and OSX the `recursive` option is used, which significantly improves the performance. +Watching for file changes is fast, since it uses Node's `recursive` option for `fs.watch()` where applicable. Any changes inside `jcr_root` folders are detected and deployed to AEM instance(s) as a package. By default, there is an exclude filter in palce: * Changes to first level directories under `jcr_root` are ingored. This is to avoid accidentally removing `apps`, `libs` or any other first level node in AEM. * Any paths containing `.svn`, `.git`, `.hg` or `target` are ignored. * The exclude filter can be overriden. Do note that this will remove the above rules completely and if required, they must be added manually. -Update interval is the time aemsync waits for file changes before the package is created. In case of multiple file changes (e.g. switching between code branches), creating a new package per file should be avoided and instead, all changes should be pushed in one go. Lowering the value decreases the delay for a single file change but may increase the delay for multiple file changes. If you are unsure, please leave the default value. +Delay is the time to wait to pass since the last change before the package is created. In case of bulk changes (e.g. switching between code branches), creating a new package per file should be avoided and instead, all changes should be pushed in one go. Lowering the value decreases the delay for a single file change but may increase the delay for multiple file changes. If you are unsure, please leave the default. ### Caveats 1. Packages are installed using package manager service (`/crx/packmgr/service.jsp`), which takes some time to initialize after AEM startup. If the push happens before, the Sling Post Servlet will take over causing the `/crx/packmgr/service.jsp/file` node to be added to the repository. Use `-c` option to performs a status check before sending (all bundles must be active). 2. Changing any XML file will cause the parent folder to be pushed. Given the many special cases around XML files, the handlig is left to the package manager. - -### Backward incompatible changes since version 4 - -1. Multiple targes are now specified with multiple `-t` options rather than a comma separated string. -2. The same goes for the exclude filter (`-e`). -3. Exclude filter supports extended globbing only. Setting exclude filter with `-e` option overrides the default. -4. JavaScript API functions have a different signature. This is to spearate mandatory and optional arguments. -5. The `push()` function returns Promise and can be resolved with `await`. diff --git a/index.js b/index.js index 461db20..4bef5a0 100644 --- a/index.js +++ b/index.js @@ -1,113 +1,301 @@ -import minimist from 'minimist' -import path from 'path' -import fs from 'fs' -import watch from 'simple-watcher' -import defaults from './src/defaults.js' -import log from './src/log.js' -import Pipeline from './src/pipeline.js' - -const { version } = JSON.parse(fs.readFileSync('./package.json', 'utf8')) - -const MSG_HELP = ` -The code and content synchronization for Sling / AEM; version ${version}. - -Usage: - aemsync [OPTIONS] - -Options: - -t URL to AEM instance; multiple can be set. - Default: ${defaults.targets} - -w Watch over folder. - Default: CWD - -p Push specific file or folder. - -e Extended glob filter; multiple can be set. - Default: - **/jcr_root/* - **/@(.git|.svn|.hg|target) - **/@(.git|.svn|.hg|target)/** - -i Update interval. - Default: ${defaults.interval} ms - -u Package manager path. - Default: ${defaults.packmgrPath} - -c Check if AEM is up and running before pushing. - -d Enable debug mode. - -h Display this screen. - -Website: - https://github.com/gavoja/aemsync -` - -function aemsync (workingDir, { targets, interval, exclude, packmgrPath, onPushEnd, checkBeforePush }) { - const pipeline = new Pipeline({ targets, interval, exclude, packmgrPath, onPushEnd, checkBeforePush }) - - pipeline.start() - watch(workingDir, localPath => { - pipeline.enqueue(localPath) - }) -} - -async function push (pathToPush, { targets, exclude, packmgrPath, checkBeforePush }) { - const pipeline = new Pipeline({ targets, exclude, packmgrPath, checkBeforePush }) - return pipeline.push(pathToPush) -} - -function main () { - const args = minimist(process.argv.slice(2)) - - // Show help. - if (args.h) { - return log.info(MSG_HELP) - } - - // Print additional debug information. - args.d && log.enableDebug() - - // Get the args. - const pathToPush = args.p ? path.resolve(args.p) : null - const workingDir = path.resolve(args.w || defaults.workingDir) - const targets = args.t ? (typeof args.t === 'string' ? [args.t] : args.t) : defaults.targets - const exclude = args.e ? (typeof args.e === 'string' ? [args.e] : args.e) : defaults.exclude - const interval = args.i || defaults.interval - const checkBeforePush = args.c - const packmgrPath = args.u || defaults.packmgrPath - - // - // Just the push. - // - - if (pathToPush) { - // Path to push does not have to exist. - // Non-existing path can be used for deletion. - return push(pathToPush, { targets }) - } - - // - // Watch mode. - // - - if (!fs.existsSync(workingDir)) { - return log.info('Invalid path:', log.gray(workingDir)) - } - - // Start aemsync - log.info(`aemsync version ${version} - - Watch over: ${log.gray(workingDir)} - Targets: ${targets.map(t => log.gray(t)).join('\n'.padEnd(17, ' '))} - Exclude: ${exclude.map(x => log.gray(x)).join('\n'.padEnd(17, ' '))} - Interval: ${log.gray(interval)} - `) - - aemsync(workingDir, { targets, interval, exclude, packmgrPath, checkBeforePush }) -} - -// Serve if run directly. -if (path.normalize(import.meta.url) === path.normalize(`file://${process.argv[1]}`)) { - main() -} - -aemsync.Pipeline = Pipeline -aemsync.main = main -aemsync.push = push - -export default aemsync +import FormData from 'form-data' +import fs from 'fs' +import minimist from 'minimist' +import fetch from 'node-fetch' +import path from 'path' +import watch from 'simple-watcher' +import xmlToJson from 'xml-to-json-stream' +import Channel from './src/channel.js' +import * as log from './src/log.js' +import Package from './src/package.js' +import ROOT from './src/root.js' + +const VERSION = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version + +console.log(VERSION) +const DEFAULTS = { + checkIfUp: false, + delay: 200, + exclude: ['**/jcr_root/*', '**/@(.git|.svn|.hg|target)', '**/@(.git|.svn|.hg|target)/**'], + packmgrPath: '/crx/packmgr/service.jsp', + postHandler: post, + targets: ['http://admin:admin@localhost:4502'], + verbose: false, + workingDir: '.' +} + +const HELP = ` +The code and content synchronization for Sling / AEM; version ${VERSION}. + +Usage: + aemsync [OPTIONS] + +Options: + -t URL to AEM instance; multiple can be set. + Default: ${DEFAULTS.targets} + -w Watch over folder. + Default: ${DEFAULTS.workingDir} + -p Push specific file or folder. + -e Extended glob filter; multiple can be set. + Default: + **/jcr_root/* + **/@(.git|.svn|.hg|target) + **/@(.git|.svn|.hg|target)/** + -d Time to wait since the last change before push. + Default: ${DEFAULTS.interval} ms + -q Package manager path. + Default: ${DEFAULTS.packmgrPath} + -c Check if AEM is up and running before pushing. + -v Enable verbose mode. + -h Display this screen. + +Examples: + Magic: + > aemsync + Custom targets: + > aemsync -t http://admin:admin@localhost:4502 -t http://admin:admin@localhost:4503 -w ~/workspace/my_project + Custom exclude rules: + > aemsync -e **/*.orig -e **/test -e -e **/test/** + Just push, don't watch: + > aemsync -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component + +Website: + https://github.com/gavoja/aemsync +` + +// ============================================================================= +// Posting to AEM. +// ============================================================================= + +async function post ({ archivePath, target, packmgrPath, checkIfUp }) { + const url = target + packmgrPath + const form = new FormData() + form.append('file', fs.createReadStream(archivePath)) + form.append('force', 'true') + form.append('install', 'true') + + // Check if AEM is up and runnig. + if (checkIfUp && !await check(target)) { + return { target, err: new Error('AEM not ready') } + } + + const result = { target } + try { + const res = await fetch(url, { method: 'POST', body: form }) + + if (res.ok) { + const text = await res.text() + + // Handle errors with AEM response. + try { + const obj = await parseXml(text) + result.log = obj.crx.response.data.log + const errorLines = [...new Set(result.log.split('\n').filter(line => line.startsWith('E')))] + + // Errors when installing selected nodes. + if (errorLines.length) { + result.err = new Error('Error installing nodes:\n' + errorLines.join('\n')) + // Error code in status. + } else if (obj.crx.response.status.code !== '200') { + result.err = new Error(obj.crx.response.status.textNode) + } + } catch (err) { + // Unexpected response format. + throw new Error('Unexpected response text format') + } + } else { + // Handle errors with the failed request. + result.err = new Error(res.statusText) + } + } catch (err) { + // Handle unexpeted errors. + result.err = err + } + + return result +} + +async function check (target) { + try { + const res = await fetch(target) + return res.status === 200 + } catch (err) { + log.debug(err.message) + return false + } +} + +function parseXml (xml) { + return new Promise(resolve => { + xmlToJson().xmlToJson(xml, (err, json) => err ? resolve({}) : resolve(json)) + }) +} + +// ============================================================================= +// Main API. +// ============================================================================= + +async function wait (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +export async function * push (args) { + const { payload, exclude, targets, packmgrPath, checkIfUp, postHandler, delay, breakStuff } = { ...DEFAULTS, ...args } + + // Get archive as many times as necessary. + let archive + while (true) { + const pack = new Package(exclude) + for (const localPath of payload) { + const item = pack.add(localPath) + item && log.info(item.exists ? '+' : '-', item.zipPath) + } + + // Ability to break stuff when testing. + // This is to simulate changes between change reported and archive creation. + breakStuff && await breakStuff() + + archive = pack.save() + if (archive.err) { + log.debug(archive.err) + await wait(delay) + log.info('Failed to create ZIP, retrying...') + } else { + break + } + } + + // Archive may not be created if items added are on the exclude path. + if (archive.path) { + for (const target of targets) { + const response = await postHandler({ archivePath: archive.path, target, packmgrPath, checkIfUp }) + log.info(log.gray(`${response.target} > ${response.err ? response.err.message : 'OK'}`)) + yield { archive, response } + } + } else { + yield {} + } +} + +export async function * aemsync (args) { + const { workingDir, delay } = { ...DEFAULTS, ...args } + const channel = new Channel() + const payload = [] + let timeoutId + + // Process file changes in the background. + ;(async function () { + for await (const localPath of watch(workingDir)) { + payload.push(localPath) + + // Graceful handling of bulk changes. + // Process only after a certain amount of time passes since the last change. + clearTimeout(timeoutId) + timeoutId = setTimeout(async () => { + // Make sure only current batch of payload is processed. + const batch = payload.splice(0, payload.length) + + for await (const result of push({ ...args, payload: batch })) { + channel.put(result) + } + }, delay) + } + })() + + // Yield results via channel. + while (true) { + yield await channel.take() + } +} + +// ============================================================================= +// CLI handling. +// ============================================================================= + +function debugResult (result) { + log.debug('Package contents:') + log.group() + log.debug(JSON.stringify(result?.archive?.contents, null, 2)) + log.groupEnd() + log.debug('Response log:') + log.group() + log.debug(result?.response?.log) + log.groupEnd() +} + +function getArgs () { + const args = minimist(process.argv.slice(2), { + default: { + w: DEFAULTS.workingDir, + t: DEFAULTS.targets, + e: DEFAULTS.exclude, + d: DEFAULTS.delay, + c: DEFAULTS.checkIfUp, + q: DEFAULTS.packmgrPath, + v: DEFAULTS.verbose + } + }) + + return { + payload: args.p ? path.resolve(args.p) : null, + workingDir: path.resolve(args.w), + targets: Array.isArray(args.t) ? args.t : [args.t], + exclude: Array.isArray(args.e) ? args.e : [args.e], + delay: args.d, + checkIfUp: args.c, + packmgrPath: args.q, + help: args.h, + verbose: args.v + } +} + +async function main () { + const args = getArgs() + + // Show help. + if (args.help) { + log.info(HELP) + return + } + + // Print additional debug information. + args.verbose && log.enableDebug() + + // + // Just the push. + // + + // Path to push does not have to exist. + // Non-existing path can be used for deletion. + if (args.payload) { + for await (const result of push(args)) { + debugResult(result) + } + } + + // + // Watch mode. + // + + if (!fs.existsSync(args.workingDir)) { + log.info('Invalid path:', log.gray(args.workingDir)) + return + } + + // Start aemsync. + log.info(`aemsync version ${VERSION} + + Watch over: ${log.gray(args.workingDir)} + Targets: ${args.targets.map(t => log.gray(t)).join('\n'.padEnd(17, ' '))} + Exclude: ${args.exclude.map(x => log.gray(x)).join('\n'.padEnd(17, ' '))} + Delay: ${log.gray(args.delay)} + `) + + for await (const result of aemsync(args)) { + debugResult(result) + } +} + +if (path.normalize(import.meta.url) === path.normalize(`file://${process.argv[1]}`)) { + main() +} diff --git a/package-lock.json b/package-lock.json index bf350c2..cf802ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1653,9 +1653,9 @@ } }, "simple-watcher": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/simple-watcher/-/simple-watcher-4.0.2.tgz", - "integrity": "sha512-PoEWcX7QBNkuJyE5IVzYUYsY6Pg55z1Skxu3a/IfLJNNy+Jxd85UMPkeqZFWcOf/n2kOKJNwqkSTAybUGQKtsw==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/simple-watcher/-/simple-watcher-5.0.0.tgz", + "integrity": "sha512-SV8gx7T1yuTHBCteL61ARpKHXsaAegkw7AI9NkZcLJYFYv6XABh3XUX26z5XIxpTsSeKWtC3OsFPslBGoX4p+w==" }, "slice-ansi": { "version": "2.1.0", diff --git a/package.json b/package.json index 3733eae..94124ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aemsync", - "version": "4.0.3", + "version": "5.0.0", "description": "The code and content synchronization for Sling / AEM (Adobe Experience Manager).", "author": "Michal Kochel ", "type": "module", @@ -30,7 +30,7 @@ "globrex": "0.1.2", "minimist": "1.2.5", "node-fetch": "2.6.1", - "simple-watcher": "4.0.2", + "simple-watcher": "5.0.0", "xml-to-json-stream": "1.1.0" }, "scripts": { diff --git a/src/channel.js b/src/channel.js new file mode 100644 index 0000000..0e7432b --- /dev/null +++ b/src/channel.js @@ -0,0 +1,34 @@ +export default class Channel { + constructor (abortSignal) { + this.messageQueue = [] + this.promiseQueue = [] + + abortSignal && abortSignal.onabort(() => { + const nextPromise = this.promiseQueue.shift() + nextPromise && nextPromise.resolve() + }) + } + + put (msg) { + // Anyone waiting for a message? + if (this.promiseQueue.length) { + // Deliver the message to the oldest one waiting (FIFO). + const nextPromise = this.promiseQueue.shift() + nextPromise.resolve(msg) + } else { + // No one is waiting - queue the event. + this.messageQueue.push(msg) + } + } + + take () { + // Do we have queued messages? + if (this.messageQueue.length) { + // Deliver the oldest queued message. + return Promise.resolve(this.messageQueue.shift()) + } else { + // No queued messages - queue the taker until a message arrives. + return new Promise((resolve, reject) => this.promiseQueue.push({ resolve, reject })) + } + } +} diff --git a/src/defaults.js b/src/defaults.js deleted file mode 100644 index 003438b..0000000 --- a/src/defaults.js +++ /dev/null @@ -1,8 +0,0 @@ -export default { - workingDir: '.', - exclude: ['**/jcr_root/*', '**/@(.git|.svn|.hg|target)', '**/@(.git|.svn|.hg|target)/**'], - packmgrPath: '/crx/packmgr/service.jsp', - targets: ['http://admin:admin@localhost:4502'], - interval: 300, - checkBeforeSend: false -} diff --git a/src/log.js b/src/log.js index 46452d1..a1f2da7 100644 --- a/src/log.js +++ b/src/log.js @@ -1,60 +1,48 @@ +let isDebug = false +let prefix = '' -import console from 'console' +function format (args, color) { + args = Array.apply(null, args) + prefix && args.unshift(prefix.slice(0, -1)) -const { Console } = console + return args.map(arg => { + if (typeof arg === 'string') { + arg = arg.replace(/\n/g, '\n' + prefix) // Handle prefix. + arg = color ? color(arg) : arg // Handle color. + } -class Log extends Console { - static getInstance () { - Log.instance = Log.instance || new Log(process.stdout, process.stderr) - return Log.instance - } - - constructor (stdout, stderr) { - super(stdout, stderr) - this.prefix = '' - } - - enableDebug () { - this.isDebug = true - } - - disableDebug () { - this.isDebug = false - } - - _format (args, color) { - args = Array.apply(null, args) - this.prefix && args.unshift(this.prefix.slice(0, -1)) + return arg + }) +} - return args.map(arg => { - if (typeof arg === 'string') { - arg = arg.replace(/\n/g, '\n' + this.prefix) // Handle prefix. - arg = color ? color(arg) : arg // Handle color. - } +export function enableDebug () { + isDebug = true +} - return arg - }) - } +export function disableDebug () { + isDebug = false +} - gray (text) { - return `\x1b[90m${text}\x1b[0m` - } +export function gray (text) { + return `\x1b[90m${text}\x1b[0m` +} - group () { - this.prefix += ' ' - } +export function group () { + prefix += ' ' +} - groupEnd () { - this.prefix = this.prefix.slice(0, -2) - } +export function groupEnd () { + prefix = prefix.slice(0, -2) +} - info () { - this.log.apply(this, this._format(arguments)) - } +export function info () { + console.log(...format(arguments)) +} - debug () { - this.isDebug && super.log.apply(this, this._format(arguments, this.gray)) - } +export function error () { + console.error(...format(arguments)) } -export default Log.getInstance() +export function debug () { + isDebug && console.log(...format(arguments, this.gray)) +} diff --git a/src/package.js b/src/package.js index 1d19ae5..35529fe 100644 --- a/src/package.js +++ b/src/package.js @@ -1,14 +1,13 @@ -'use strict' - -import util from 'util' import fs from 'fs' -import path from 'path' import globrex from 'globrex' -import log from './log.js' -import defaults from './defaults.js' +import path from 'path' +import util from 'util' +import * as log from './log.js' import Zip from './zip.js' +import ROOT from './root.js' -const DATA_PATH = path.resolve('./data') +// Get current script's directory. +const DATA_PATH = path.resolve(ROOT, 'data') const PACKAGE_CONTENT_PATH = path.join(DATA_PATH, 'package-content') const NT_FOLDER_PATH = path.join(DATA_PATH, 'nt-folder', '.content.xml') const FILTER_ZIP_PATH = 'META-INF/vault/filter.xml' @@ -26,7 +25,7 @@ const FILTER_CHILDREN = ` // https://jackrabbit.apache.org/filevault/vaultfs.html export default class Package { - constructor (exclude = defaults.exclude) { + constructor (exclude) { this.zip = new Zip() this.exclude = exclude || [] this.entries = [] @@ -57,7 +56,7 @@ export default class Package { return null } - // If folder, Add missing .content.xml@nt:folder inside. + // If folder, add missing .content.xml@nt:folder inside. // This ensures proper handlig when removing inner .content.xml file. this._addContentXml(localPath) @@ -140,47 +139,44 @@ export default class Package { save (archivePath) { if (this.entries.length === 0) { - return null + return {} } - // Create archive and add default package content. - const jcrRoot = path.join(PACKAGE_CONTENT_PATH, 'jcr_root') - const metaInf = path.join(PACKAGE_CONTENT_PATH, 'META-INF') - this.zip.add(jcrRoot, 'jcr_root') - this.zip.add(metaInf, 'META-INF') - - // Add each entry. - const filters = [] - for (const entry of this.entries) { - if (!entry.exists) { - // DELETE - // Only filters need to be updated. - filters.push(util.format(FILTER, entry.filterPath)) - } else { - // ADD - // Filters need to be updated. - const dirName = path.dirname(entry.filterPath) - // if (!entry.localPath.endsWith('.content.xml')) { - filters.push(util.format(FILTER_CHILDREN, dirName, dirName, entry.filterPath, entry.filterPath)) - // } - - // ADD - // File or folder needs to be added to the zip. - this.zip.add(entry.localPath, entry.zipPath) + try { + // Create archive and add default package content. + const jcrRoot = path.join(PACKAGE_CONTENT_PATH, 'jcr_root') + const metaInf = path.join(PACKAGE_CONTENT_PATH, 'META-INF') + this.zip.add(jcrRoot, 'jcr_root') + this.zip.add(metaInf, 'META-INF') + + // Add each entry. + const filters = [] + for (const entry of this.entries) { + if (!entry.exists) { + // DELETE + // Only filters need to be updated. + filters.push(util.format(FILTER, entry.filterPath)) + } else { + // ADD + // Filters need to be updated. + const dirName = path.dirname(entry.filterPath) + filters.push(util.format(FILTER_CHILDREN, dirName, dirName, entry.filterPath, entry.filterPath)) + + // ADD + // File or folder needs to be added to the zip. + this.zip.add(entry.localPath, entry.zipPath) + } } - } - // Add filter file. - const filter = util.format(FILTER_WRAPPER, filters.join('\n')) - this.zip.add(Buffer.from(filter), FILTER_ZIP_PATH) + // Add filter file. + const filter = util.format(FILTER_WRAPPER, filters.join('\n')) + this.zip.add(Buffer.from(filter), FILTER_ZIP_PATH) - // Debug package contents. - log.debug('Package details:') - log.group() - log.debug(JSON.stringify(this.zip.inspect(), null, 2)) - log.groupEnd() - - return this.zip.save(archivePath) + return { path: this.zip.save(archivePath), contents: this.zip.inspect() } + } catch (err) { + // Can happen in case files change in the meanwhile. + return { err } + } } // diff --git a/src/pipeline.js b/src/pipeline.js deleted file mode 100644 index 1d77bf8..0000000 --- a/src/pipeline.js +++ /dev/null @@ -1,143 +0,0 @@ -'use strict' - -import fs from 'fs' -import fetch from 'node-fetch' -import FormData from 'form-data' -import xmlToJson from 'xml-to-json-stream' -import Package from './package.js' -import log from './log.js' -import defaults from './defaults.js' - -export default class Pipeline { - constructor (opts = {}) { - this.lock = false - this.queue = [] - this.checkBeforePush = opts.checkBeforePush || defaults.checkBeforePush - this.packmgrPath = opts.packmgrPath || defaults.packmgrPath - this.targets = opts.targets || defaults.target - this.interval = opts.interval || defaults.interval - this.exclude = opts.exclude || defaults.exclude - this.onPushEnd = opts.onPushEnd || function () {} - } - - start () { - setInterval(async () => { - await this._processQueue() - }, this.interval) - } - - enqueue (localPath) { - log.debug(`Changed: ${localPath}`) - this.queue.push(localPath) - } - - async push (pathToPush) { - this.enqueue(pathToPush) - return this._processQueue() - } - - async _processQueue () { - // Wait for the previous package to install. - // Otherwise an error may occur if two concurrent packages try to make - // changes to the same node. - if (this.lock === true || this.queue.length < 1) { - return null - } - - // Lock the queue. - this.lock = true - - // Create package. - const pack = new Package(this.exclude) - while (this.queue.length > 0) { - const localPath = this.queue.pop() - const item = pack.add(localPath) - item && log.info(item.exists ? '+' : '-', item.zipPath) - } - - // Push package to targets (if any entries detected). - log.group() - const archivePath = pack.save() - if (archivePath) { - for (const target of this.targets) { - const result = await this._post(archivePath, target) - this.onPushEnd(result.err, result.target, result.log) - log.info(log.gray(target + ' >'), log.gray(result.err ? result.err.message : 'OK')) - } - } - log.groupEnd() - - // Release lock. - this.lock = false - - return pack - } - - async _post (archivePath, target) { - const url = target + this.packmgrPath - const form = new FormData() - form.append('file', fs.createReadStream(archivePath)) - form.append('force', 'true') - form.append('install', 'true') - - // Check if AEM is up and runnig. - if (this.checkBeforePush && !await this._check(target)) { - return { target, err: new Error('AEM not ready') } - } - - const result = { target } - try { - const res = await fetch(url, { method: 'POST', body: form }) - - if (res.ok) { - const text = await res.text() - log.debug('Response text:') - log.group() - log.debug(text) - log.groupEnd() - - // Handle errors with AEM response. - try { - const obj = await this._parseXml(text) - result.log = obj.crx.response.data.log - const errorLines = [...new Set(result.log.split('\n').filter(line => line.startsWith('E')))] - - // Errors when installing selected nodes. - if (errorLines.length) { - result.err = new Error('Error installing nodes:\n' + errorLines.join('\n')) - // Error code in status. - } else if (obj.crx.response.status.code !== '200') { - result.err = new Error(obj.crx.response.status.textNode) - } - } catch (err) { - // Unexpected response format. - throw new Error('Unexpected response text format') - } - } else { - // Handle errors with the failed request. - result.err = new Error(res.statusText) - } - } catch (err) { - // Handle unexpeted errors. - result.err = err - } - - return result - } - - async _check (target) { - try { - const res = await fetch(target) - return res.status === 200 - } catch (err) { - log.debug(err.message) - return false - } - } - - _parseXml (xml) { - return new Promise(resolve => { - xmlToJson().xmlToJson(xml, (err, json) => err ? resolve({}) : resolve(json)) - }) - } -} diff --git a/src/root.js b/src/root.js new file mode 100644 index 0000000..2ae1ed1 --- /dev/null +++ b/src/root.js @@ -0,0 +1,4 @@ +import { resolve, dirname } from 'path' +import { fileURLToPath } from 'url' + +export default resolve(dirname(fileURLToPath(import.meta.url)), '..') diff --git a/src/zip.js b/src/zip.js index 5cb78a1..bbad3f2 100644 --- a/src/zip.js +++ b/src/zip.js @@ -1,9 +1,7 @@ -'use strict' - -import path from 'path' -import os from 'os' -import fs from 'fs' import AdmZip from 'adm-zip' +import fs from 'fs' +import os from 'os' +import path from 'path' const DEFAULT_ARCHIVE_PATH = path.join(os.tmpdir(), 'aemsync.zip') @@ -98,7 +96,7 @@ export default class Zip { } } - return entries + return entries.sort() } _getFilter () { diff --git a/test/jcr_root/_cq_design_dialog.xml b/test/jcr_root/apps/myapp/component/_cq_design_dialog.xml similarity index 100% rename from test/jcr_root/_cq_design_dialog.xml rename to test/jcr_root/apps/myapp/component/_cq_design_dialog.xml diff --git a/test/test.js b/test/test.js index a20a42b..3a273e9 100644 --- a/test/test.js +++ b/test/test.js @@ -1,75 +1,55 @@ 'use strict' -import test from 'triala' -import path from 'path' import assert from 'assert' -import aemsync from '../index.js' +import path from 'path' +import test from 'triala' +import fs from 'fs' +import { push } from '../index.js' const COMPONENT = path.resolve('./test/jcr_root/apps/myapp/component') -const TARGET = 'http://admin:admin@localhost:1234' test('aemsync', class { - async _before () { - this.pipeline = new aemsync.Pipeline({ targets: [TARGET] }) - } + // Push wrapper with overloaded POST handler. + async _push (entry, breakStuff) { + const payload = [entry] + const args = { payload, breakStuff, postHandler: () => ({ target: 'http://test.local' }) } + const results = [] + for await (const result of push(args)) { + results.push(result) + } - async _push (pathToPush) { - const pack = await this.pipeline.push(pathToPush) - return pack.zip.inspect() + return results.shift()?.archive?.contents } // // Test cases start here. // - async 'onPushEnd failure' () { - const msg = 'Something went wrong' - this.pipeline._post = (archivePath, target) => ({ err: new Error(msg), target }) - - let error = null - this.pipeline.onPushEnd = (err, target, log) => (error = err) - - await this.pipeline.push(COMPONENT) - - // Reset for the rest of the tests. - this.pipeline.onPushEnd = () => {} - this.pipeline._post = (archivePath, target) => ({ target }) - - // Check if error message matches. - assert.strictEqual(error.message, msg) - } - async 'exclude' () { - const expected = { entries: [], filter: [''] } - - assert.deepStrictEqual(await this._push(path.join('jcr_root')), expected) - assert.deepStrictEqual(await this._push(path.join('jcr_root', 'bar')), expected) - assert.deepStrictEqual(await this._push(path.join('foo', 'jcr_root')), expected) - assert.deepStrictEqual(await this._push(path.join('foo', 'jcr_root', 'bar')), expected) - assert.deepStrictEqual(await this._push(path.join('.svn')), expected) - assert.deepStrictEqual(await this._push(path.join('.hg')), expected) - assert.deepStrictEqual(await this._push(path.join('.git')), expected) - assert.deepStrictEqual(await this._push(path.join('target')), expected) - assert.deepStrictEqual(await this._push(path.join('foo', '.svn')), expected) - assert.deepStrictEqual(await this._push(path.join('foo', '.hg')), expected) - assert.deepStrictEqual(await this._push(path.join('foo', '.git')), expected) - assert.deepStrictEqual(await this._push(path.join('foo', 'target')), expected) - assert.deepStrictEqual(await this._push(path.join('foo', '.svn', 'bar')), expected) - assert.deepStrictEqual(await this._push(path.join('foo', '.hg', 'bar')), expected) - assert.deepStrictEqual(await this._push(path.join('foo', '.git', 'bar')), expected) - assert.deepStrictEqual(await this._push(path.join('foo', 'target', 'bar')), expected) - assert.ok((await this._push(path.join('foo', 'jcr_root', 'bar', 'baz'))).entries.length > 0) + const expected = undefined + + assert.deepStrictEqual(await this._push('jcr_root'), expected) + assert.deepStrictEqual(await this._push('jcr_root/bar'), expected) + assert.deepStrictEqual(await this._push('foo/jcr_root'), expected) + assert.deepStrictEqual(await this._push('foo/jcr_root/bar'), expected) + assert.deepStrictEqual(await this._push('.svn'), expected) + assert.deepStrictEqual(await this._push('.hg'), expected) + assert.deepStrictEqual(await this._push('.git'), expected) + assert.deepStrictEqual(await this._push('target'), expected) + assert.deepStrictEqual(await this._push('foo/.svn'), expected) + assert.deepStrictEqual(await this._push('foo/.hg'), expected) + assert.deepStrictEqual(await this._push('foo/.git'), expected) + assert.deepStrictEqual(await this._push('foo/target'), expected) + assert.deepStrictEqual(await this._push('foo/.svn/bar'), expected) + assert.deepStrictEqual(await this._push('foo/.hg/bar'), expected) + assert.deepStrictEqual(await this._push('foo/.git/bar'), expected) + assert.deepStrictEqual(await this._push('foo/target/bar'), expected) + assert.ok((await this._push('foo/jcr_root/bar/baz')).entries) } async '+ file.txt' () { const expected = { entries: [ - 'jcr_root/', - 'jcr_root/aemsync.txt', - 'jcr_root/apps/.content.xml@nt:folder', - 'jcr_root/apps/myapp/.content.xml@nt:folder', - 'jcr_root/apps/myapp/component/.content.xml@cq:Component', - 'jcr_root/apps/myapp/component/file.txt', 'META-INF/', 'META-INF/vault/', 'META-INF/vault/config.xml', @@ -77,7 +57,13 @@ test('aemsync', class { 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', 'META-INF/vault/filter.xml', 'META-INF/vault/nodetypes.cnd', - 'META-INF/vault/properties.xml' + 'META-INF/vault/properties.xml', + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'jcr_root/apps/myapp/component/file.txt' ], filter: [ '', @@ -109,18 +95,13 @@ test('aemsync', class { ] } - const result = await this._push(path.join(COMPONENT, 'file.txt')) + const result = await this._push(`${COMPONENT}/file.txt`) assert.deepStrictEqual(result, expected) } async '+ folder' () { const expected = { entries: [ - 'jcr_root/', - 'jcr_root/aemsync.txt', - 'jcr_root/apps/.content.xml@nt:folder', - 'jcr_root/apps/myapp/.content.xml@nt:folder', - 'jcr_root/apps/myapp/component/.content.xml@cq:Component', 'META-INF/', 'META-INF/vault/', 'META-INF/vault/config.xml', @@ -128,7 +109,12 @@ test('aemsync', class { 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', 'META-INF/vault/filter.xml', 'META-INF/vault/nodetypes.cnd', - 'META-INF/vault/properties.xml' + 'META-INF/vault/properties.xml', + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component' ], filter: [ '', @@ -156,18 +142,13 @@ test('aemsync', class { ] } - const result = await this._push(path.join(COMPONENT, 'folder')) + const result = await this._push(`${COMPONENT}/folder`) assert.deepStrictEqual(result, expected) } async '+ sub-folder' () { const expected = { entries: [ - 'jcr_root/', - 'jcr_root/aemsync.txt', - 'jcr_root/apps/.content.xml@nt:folder', - 'jcr_root/apps/myapp/.content.xml@nt:folder', - 'jcr_root/apps/myapp/component/.content.xml@cq:Component', 'META-INF/', 'META-INF/vault/', 'META-INF/vault/config.xml', @@ -175,7 +156,12 @@ test('aemsync', class { 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', 'META-INF/vault/filter.xml', 'META-INF/vault/nodetypes.cnd', - 'META-INF/vault/properties.xml' + 'META-INF/vault/properties.xml', + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component' ], filter: [ '', @@ -203,19 +189,28 @@ test('aemsync', class { ] } - const result = await this._push(path.join(COMPONENT, 'folder', 'sub-folder')) + const result = await this._push(`${COMPONENT}/folder/sub-folder`) assert.deepStrictEqual(result, expected) } - async '+ component, + file.xml, + .content.xml, + file-node.xml, - deleted.xml' () { + async '+ component, + file.xml, + .content.xml, + file-node.xml, + cq_design_dialog.xml, - deleted.xml' () { const expected = { entries: [ + 'META-INF/', + 'META-INF/vault/', + 'META-INF/vault/config.xml', + 'META-INF/vault/definition/', + 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', + 'META-INF/vault/filter.xml', + 'META-INF/vault/nodetypes.cnd', + 'META-INF/vault/properties.xml', 'jcr_root/', 'jcr_root/aemsync.txt', 'jcr_root/apps/.content.xml@nt:folder', 'jcr_root/apps/myapp/.content.xml@nt:folder', 'jcr_root/apps/myapp/component/', 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'jcr_root/apps/myapp/component/_cq_design_dialog.xml', 'jcr_root/apps/myapp/component/_jcr_content/', 'jcr_root/apps/myapp/component/_jcr_content/.content.xml@cq:PageContent', 'jcr_root/apps/myapp/component/_jcr_content/file-node.xml', @@ -232,15 +227,7 @@ test('aemsync', class { 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/.content.xml@nt:unstructured', 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/file-node.xml', 'jcr_root/apps/myapp/component/folder-node/', - 'jcr_root/apps/myapp/component/folder-node/.content.xml@nt:unstructured', - 'META-INF/', - 'META-INF/vault/', - 'META-INF/vault/config.xml', - 'META-INF/vault/definition/', - 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', - 'META-INF/vault/filter.xml', - 'META-INF/vault/nodetypes.cnd', - 'META-INF/vault/properties.xml' + 'jcr_root/apps/myapp/component/folder-node/.content.xml@nt:unstructured' ], filter: [ '', @@ -272,32 +259,28 @@ test('aemsync', class { ] } - let result = await this._push(path.join(COMPONENT)) + let result = await this._push(COMPONENT) assert.deepStrictEqual(result, expected) - result = await this._push(path.join(COMPONENT, 'file.xml')) + result = await this._push(`${COMPONENT}/file.xml`) assert.deepStrictEqual(result, expected) - result = await this._push(path.join(COMPONENT, '.content.xml')) + result = await this._push(`${COMPONENT}/content.xml`) assert.deepStrictEqual(result, expected) - result = await this._push(path.join(COMPONENT, 'file-node.xml')) + result = await this._push(`${COMPONENT}/file-node.xml`) assert.deepStrictEqual(result, expected) - result = await this._push(path.join(COMPONENT, 'deleted.xml')) + result = await this._push(`${COMPONENT}/_cq_design_dialog.xml`) + assert.deepStrictEqual(result, expected) + + result = await this._push(`${COMPONENT}/deleted.xml`) assert.deepStrictEqual(result, expected) } async '+ folder-node, + folder-node/.content.xml' () { const expected = { entries: [ - 'jcr_root/', - 'jcr_root/aemsync.txt', - 'jcr_root/apps/.content.xml@nt:folder', - 'jcr_root/apps/myapp/.content.xml@nt:folder', - 'jcr_root/apps/myapp/component/.content.xml@cq:Component', - 'jcr_root/apps/myapp/component/folder-node/', - 'jcr_root/apps/myapp/component/folder-node/.content.xml@nt:unstructured', 'META-INF/', 'META-INF/vault/', 'META-INF/vault/config.xml', @@ -305,7 +288,14 @@ test('aemsync', class { 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', 'META-INF/vault/filter.xml', 'META-INF/vault/nodetypes.cnd', - 'META-INF/vault/properties.xml' + 'META-INF/vault/properties.xml', + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'jcr_root/apps/myapp/component/folder-node/', + 'jcr_root/apps/myapp/component/folder-node/.content.xml@nt:unstructured' ], filter: [ '', @@ -343,16 +333,24 @@ test('aemsync', class { ] } - let result = await this._push(path.join(COMPONENT, 'folder-node')) + let result = await this._push(`${COMPONENT}/folder-node`) assert.deepStrictEqual(result, expected) - result = await this._push(path.join(COMPONENT, 'folder-node', '.content.xml')) + result = await this._push(`${COMPONENT}/folder-node/.content.xml`) assert.deepStrictEqual(result, expected) } async '+ folder-node-nested' () { const expected = { entries: [ + 'META-INF/', + 'META-INF/vault/', + 'META-INF/vault/config.xml', + 'META-INF/vault/definition/', + 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', + 'META-INF/vault/filter.xml', + 'META-INF/vault/nodetypes.cnd', + 'META-INF/vault/properties.xml', 'jcr_root/', 'jcr_root/aemsync.txt', 'jcr_root/apps/.content.xml@nt:folder', @@ -366,15 +364,7 @@ test('aemsync', class { 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/.content.xml@nt:unstructured', 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/', 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/.content.xml@nt:unstructured', - 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/file-node.xml', - 'META-INF/', - 'META-INF/vault/', - 'META-INF/vault/config.xml', - 'META-INF/vault/definition/', - 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', - 'META-INF/vault/filter.xml', - 'META-INF/vault/nodetypes.cnd', - 'META-INF/vault/properties.xml' + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/file-node.xml' ], filter: [ '', @@ -412,13 +402,21 @@ test('aemsync', class { ] } - const result = await this._push(path.join(COMPONENT, 'folder-node-nested')) + const result = await this._push(`${COMPONENT}/folder-node-nested`) assert.deepStrictEqual(result, expected) } async '+ folder-node-nested/foo/bar/baz/file-node.xml' () { const expected = { entries: [ + 'META-INF/', + 'META-INF/vault/', + 'META-INF/vault/config.xml', + 'META-INF/vault/definition/', + 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', + 'META-INF/vault/filter.xml', + 'META-INF/vault/nodetypes.cnd', + 'META-INF/vault/properties.xml', 'jcr_root/', 'jcr_root/aemsync.txt', 'jcr_root/apps/.content.xml@nt:folder', @@ -429,15 +427,7 @@ test('aemsync', class { 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/.content.xml@nt:unstructured', 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/', 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/.content.xml@nt:unstructured', - 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/file-node.xml', - 'META-INF/', - 'META-INF/vault/', - 'META-INF/vault/config.xml', - 'META-INF/vault/definition/', - 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', - 'META-INF/vault/filter.xml', - 'META-INF/vault/nodetypes.cnd', - 'META-INF/vault/properties.xml' + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/file-node.xml' ], filter: [ '', @@ -493,21 +483,13 @@ test('aemsync', class { ] } - const result = await this._push(path.join(COMPONENT, 'folder-node-nested', 'foo', 'bar', 'baz', 'file-node.xml')) + const result = await this._push(`${COMPONENT}/folder-node-nested/foo/bar/baz/file-node.xml`) assert.deepStrictEqual(result, expected) } async '+ _jcr_content, + _jcr_content/.content.xml, + _jcr_content/file-node.xml, - _jcr_content/deleted.xml' () { const expected = { entries: [ - 'jcr_root/', - 'jcr_root/aemsync.txt', - 'jcr_root/apps/.content.xml@nt:folder', - 'jcr_root/apps/myapp/.content.xml@nt:folder', - 'jcr_root/apps/myapp/component/.content.xml@cq:Component', - 'jcr_root/apps/myapp/component/_jcr_content/', - 'jcr_root/apps/myapp/component/_jcr_content/.content.xml@cq:PageContent', - 'jcr_root/apps/myapp/component/_jcr_content/file-node.xml', 'META-INF/', 'META-INF/vault/', 'META-INF/vault/config.xml', @@ -515,7 +497,15 @@ test('aemsync', class { 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', 'META-INF/vault/filter.xml', 'META-INF/vault/nodetypes.cnd', - 'META-INF/vault/properties.xml' + 'META-INF/vault/properties.xml', + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'jcr_root/apps/myapp/component/_jcr_content/', + 'jcr_root/apps/myapp/component/_jcr_content/.content.xml@cq:PageContent', + 'jcr_root/apps/myapp/component/_jcr_content/file-node.xml' ], filter: [ '', @@ -552,27 +542,23 @@ test('aemsync', class { '' ] } - let result = await this._push(path.join(COMPONENT, '_jcr_content')) + + let result = await this._push(`${COMPONENT}/_jcr_content`) assert.deepStrictEqual(result, expected) - result = await this._push(path.join(COMPONENT, '_jcr_content', 'file-node.xml')) + result = await this._push(`${COMPONENT}/_jcr_content/file-node.xml`) assert.deepStrictEqual(result, expected) - result = await this._push(path.join(COMPONENT, '_jcr_content', '.content.xml')) + result = await this._push(`${COMPONENT}/_jcr_content/.content.xml`) assert.deepStrictEqual(result, expected) - result = await this._push(path.join(COMPONENT, '_jcr_content', 'deleted.xml')) + result = await this._push(`${COMPONENT}/_jcr_content/deleted.xml`) assert.deepStrictEqual(result, expected) } async '- deleted' () { const expected = { entries: [ - 'jcr_root/', - 'jcr_root/aemsync.txt', - 'jcr_root/apps/.content.xml@nt:folder', - 'jcr_root/apps/myapp/.content.xml@nt:folder', - 'jcr_root/apps/myapp/component/.content.xml@cq:Component', 'META-INF/', 'META-INF/vault/', 'META-INF/vault/config.xml', @@ -580,7 +566,12 @@ test('aemsync', class { 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', 'META-INF/vault/filter.xml', 'META-INF/vault/nodetypes.cnd', - 'META-INF/vault/properties.xml' + 'META-INF/vault/properties.xml', + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component' ], filter: [ '', @@ -608,18 +599,13 @@ test('aemsync', class { ] } - const result = await this._push(path.join(COMPONENT, 'deleted')) + const result = await this._push(`${COMPONENT}/deleted`) assert.deepStrictEqual(result, expected) } - async '+ _cq_design_dialog.xml' () { + async '+ new-file.txt, - new-file.txt' () { const expected = { entries: [ - 'jcr_root/', - 'jcr_root/aemsync.txt', - 'jcr_root/apps/.content.xml@nt:folder', - 'jcr_root/apps/myapp/.content.xml@nt:folder', - 'jcr_root/apps/myapp/component/.content.xml@cq:Component', 'META-INF/', 'META-INF/vault/', 'META-INF/vault/config.xml', @@ -627,12 +613,17 @@ test('aemsync', class { 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', 'META-INF/vault/filter.xml', 'META-INF/vault/nodetypes.cnd', - 'META-INF/vault/properties.xml' + 'META-INF/vault/properties.xml', + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component' ], filter: [ '', '', - '', + '', '', '', '', @@ -655,7 +646,17 @@ test('aemsync', class { ] } - const result = await this._push(path.join(COMPONENT, '_cq_design_dialog')) + // Create new file. + const newFile = `${COMPONENT}/new-file.txt` + fs.writeFileSync(newFile, 'new-file.txt') + + // Delete file before push. + const result = await this._push(newFile, () => { + if (fs.existsSync(newFile)) { + fs.unlinkSync(newFile) + } + }) + assert.deepStrictEqual(result, expected) } })