Skip to content

Commit

Permalink
Implementation simplified.
Browse files Browse the repository at this point in the history
Tests added.
Dependencies updated and reduced.
Promises introduced.
Fixes for #30, #35 and #42.
  • Loading branch information
gavoja committed Apr 27, 2019
1 parent 80e7dca commit b9dac80
Show file tree
Hide file tree
Showing 34 changed files with 1,375 additions and 1,073 deletions.
15 changes: 15 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
// 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": "Test",
"program": "${workspaceFolder}\\test\\test.js",
"args": ["-v"]
}
]
}
111 changes: 70 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
aemsync
=======

AEM (Adobe CQ) Synchronization Tool.
The code and content synchronization for Sling / AEM (Adobe Experience Manager).

### Synopsis

The tool pushes code changes to AEM instance(s) upon a file change.
The tool pushes content to AEM instance(s) upon a file change.
* There is no vault dependency.
* It can push to multiple instances at the same time (e.g. author and publish).
* IDE/editor agnostic.
* IDE / editor agnostic.
* Works on Windows, Linux and Mac.

### Installation
Expand All @@ -21,88 +21,117 @@ npm install aemsync -g

### Usage

Simply run `aemsync` on your project path, make a change to any of your files or directories and watch the magic happen.

### Advanced usage

Commandline
```
Usage:
aemsync [OPTIONS]
Options:
-t <targets> Defult is http://admin:admin@localhost:4502
-w <path_to_watch> Default is current
-p <path_to_push> Path to push directly; used instead of above,
no watching takes place
-e <exclude_filter> Micromatch exclude filter; disabled by default
-i <sync_interval> Update interval; default is 300ms
-u <packmgr_path> Package manager path; default is
/crx/packmgr/service.jsp
-d Enable debug mode
-h Displays this screen
```
```
aemsync -t http://admin:admin@localhost:4502,http://admin:admin@localhost:4503 -w ~/workspace/my_project
-t <target> URL to AEM instance; multiple can be set.
Default: ${defaults.targets}
-w <path_to_watch> Watch over folder.
Default: CWD
-p <path_to_push> Push specific file or folder.
-e <exclude_filter> Extended glob filter; multiple can be set.
Default:
**/jcr_root/*
**/@(.git|.svn|.hg|target)
**/@(.git|.svn|.hg|target)/**
-i <sync_interval> Update interval.
Default: ${defaults.interval} ms
-u <packmgr_path> Package manager path.
Default: ${defaults.packmgrPath}
-d Enable debug 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 **/@(.git) -e **/@(.git)/** -e '**/*.orig'
Just push, don't watch:
> aemsync -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component
```

JavaScript (full watch example):
```JavaScript
// Import aemsync.
const aemsync = require('aemsync')

// Set up the environment.
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 interval = 300
const exclude = ['**/*.orig'] // Skip merge files.
const packmgrUrl = '/foo/crx/packmgr/service.jsp'
const onPushEnd = (err, host) => {
const interval = 300
const onPushEnd = (err, target, log) => {
// Called for each of the targets.
if (err) {
return console.log(`Error when pushing package to ${host}.`, err)
console.log(`Error when pushing package to ${target}.`, err.message)
} else {
console.log(`Package pushed to ${target}. Response log:\n${target.log}`)
}
console.log(`Package pushed to ${host}.`)
}

// Will watch for changes on workingDir and push them.
aemsync({workingDir, targets, exclude, interval, packmgrUrl, onPushEnd})
// Will watch for changes on workingDir and push them upon a file change.
aemsync(workingDir, { targets, exclude, interval, packmgrUrl, onPushEnd })
```

JavaScript (direct push example):
```JavaScript
// Import aemsync.
const aemsync = require('aemsync')
const { push } = require('aemsync')

const pathToPush = '~/foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component'

// Set up the environment.
const path = '~/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, host) => {
const onPushEnd = (err, target, log) => {
// Called for each of the targets.
if (err) {
return console.log(`Error when pushing package to ${host}.`, err)
console.log(`Error when pushing package to ${target}.`, err.message)
} else {
console.log(`Package pushed to ${target}. Response log:\n${target.log}`)
}
console.log(`Package pushed to ${host}.`)
}

// Will push the path to AEM.
aemsync.push({path, targets, onPushEnd})
// 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().
await push(pathToPush, { targets, onPushEnd })
```

### 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.
Any changes inside `jcr_root` folders are detected and deployed to AEM instance(s) as a package. Rules:

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.
* The following are ignored by default: `.svn`, `.git`, `.hg`.
* 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 the Pusher 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.

Note that some of the file changes will result in pushing the entire parent folder:
* Ading, removing or renaming files or directories.
* Changing `.content.xml`.
* Changing any file or directory inside `nt:unstructured` subtree. In this case the first non `nt:unstructured` ancestor will be pushed. This behaviour ensures proper handling of self-contained unstructured blocks of nodes such as dialogs that are distributed across multiple files (see [issue 19](https://github.com/gavoja/aemsync/issues/19)).
### 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.
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.

### Known issues
### Backward incompatible changes since version 4

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.
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`.
File renamed without changes.
3 changes: 0 additions & 3 deletions data/nt_file/.content.xml

This file was deleted.

File renamed without changes.
102 changes: 57 additions & 45 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,53 @@

const minimist = require('minimist')
const path = require('path')
const fs = require('graceful-fs')
const fs = require('fs')
const watch = require('simple-watcher')
const defaults = require('./src/defaults')
const log = require('./src/log')
const chalk = require('chalk')
const Watcher = require('./src/watcher')
const Pipeline = require('./src/pipeline')
const { version } = require('./package.json')

const MSG_HELP = `
The code and content synchronization for Sling / AEM; version ${version}.
Usage:
aemsync [OPTIONS]
Options:
-t <targets> Defult is http://admin:admin@localhost:4502
-w <path_to_watch> Default is current
-p <path_to_push> Path to push directly; used instead of above,
no watching takes place
-e <exclude_filter> Micromatch exclude filter; disabled by default
-i <sync_interval> Update interval; default is 300ms
-u <packmgr_path> Package manager path; default is
/crx/packmgr/service.jsp
-d Enable debug mode
-h Displays this screen
-t <target> URL to AEM instance; multiple can be set.
Default: ${defaults.targets}
-w <path_to_watch> Watch over folder.
Default: CWD
-p <path_to_push> Push specific file or folder.
-e <exclude_filter> Extended glob filter; multiple can be set.
Default:
**/jcr_root/*
**/@(.git|.svn|.hg|target)
**/@(.git|.svn|.hg|target)/**
-i <sync_interval> Update interval.
Default: ${defaults.interval} ms
-u <packmgr_path> Package manager path.
Default: ${defaults.packmgrPath}
-d Enable debug mode.
-h Display this screen.
Website:
https://github.com/gavoja/aemsync
`

function aemsync (args) {
const pipeline = new Pipeline(args)
const watcher = new Watcher()
function aemsync (workingDir, { targets, interval, exclude, packmgrPath, onPushEnd }) {
const pipeline = new Pipeline({ targets, interval, exclude, packmgrPath, onPushEnd })

pipeline.start()

args.callback = (localPath) => {
watch(workingDir, localPath => {
pipeline.enqueue(localPath)
}

watcher.watch(args)
})
}

function push (args) {
const pipeline = new Pipeline(args)
pipeline.push(args.pathToPush)
async function push (pathToPush, { targets, exclude, packmgrPath }) {
const pipeline = new Pipeline({ targets, exclude, packmgrPath })
return pipeline.push(pathToPush)
}

function main () {
Expand All @@ -54,44 +59,51 @@ function main () {
return console.log(MSG_HELP)
}

// Get other args.
log.isDebug = args.d
const workingDir = path.resolve(args.w || '.')
const targets = (args.t || 'http://admin:admin@localhost:4502').split(',')
const interval = args.i || 300
const exclude = args.e || ''
const packmgrPath = args.u
// 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 packmgrPath = args.u || defaults.packmgrPath

//
// Just the push.
if (args.p) {
let pathToPush = path.resolve(args.p)
if (!fs.existsSync(pathToPush)) {
return log.info('Invalid path:', chalk.yellow(workingDir))
}
//

return push({pathToPush, targets})
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:', chalk.yellow(workingDir))
return log.info('Invalid path:', log.gray(workingDir))
}

// Start aemsync
log.info(`
Working dir: ${chalk.yellow(workingDir)}
Targets: ${chalk.yellow(targets)}
Interval: ${chalk.yellow(interval)}
Exclude: ${chalk.yellow(exclude)}
log.info(`aemsync version ${version}
Watch over: ${log.gray(workingDir)}
Targets: ${log.gray(targets.map((t, ii) => ii === 0 ? t : ''.padStart(16, ' ') + t).join('\n'))}
Exclude: ${log.gray(exclude.map((x, ii) => ii === 0 ? x : ''.padStart(16, ' ') + x).join('\n'))}
Interval: ${log.gray(interval)}
`)

aemsync({workingDir, targets, interval, exclude, packmgrPath})
aemsync(workingDir, { targets, interval, exclude, packmgrPath })
}

if (require.main === module) {
main()
}

aemsync.Watcher = Watcher
aemsync.Pipeline = Pipeline
aemsync.main = main
aemsync.push = push
Expand Down
Loading

0 comments on commit b9dac80

Please sign in to comment.