diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..810336c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,39 @@ +name: Test + +on: + release: + types: [published] + pull_request: + branches: + - master + push: + branches: + - master + +env: + NODE_VERSION: "22.x" + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: "**/package-lock.json" + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Run tests + run: npm test diff --git a/CHANGELOG.md b/CHANGELOG.md index e55b984..a084a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 5.2.1 + +- Moved CLI to a separate module. +- Prefixed Node modules with `node:`. +- Updated README.md. +- Added GitHub test workflow. + ## 5.1.1 - Added file system exclusions. diff --git a/README.md b/README.md index 2f38283..8a1734d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -aemsync -======= +# aemsync + The code and content synchronization for Sling / AEM (Adobe Experience Manager). -### Synopsis +## Synopsis The tool pushes content to AEM instance(s) upon a file change. @@ -12,7 +12,7 @@ The tool pushes content to AEM instance(s) upon a file change. * IDE / editor agnostic. * Works on Windows, Linux, and Mac. -### Installation +## Installation With [npm](http://npmjs.org) do: @@ -20,13 +20,14 @@ With [npm](http://npmjs.org) do: npm install aemsync -g ``` -### Usage +## 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 +## Advanced usage + +### CLI -CLI ``` Usage: aemsync [OPTIONS] @@ -66,41 +67,60 @@ Website: https://github.com/gavoja/aemsync ``` -JavaScript API -```JavaScript -import { aemsync, push } from 'aemsync' +### API + +Watch mode: -// Interactive watch example. -const args = { workingDir } +```js +import { aemsync } from 'aemsync' + +const args = { workingDir: 'c:/code/my-aem-project' } for await (const result of aemsync(args)) { console.log(result) } +``` + +Push: -// Push example. -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' -]} +```js +import { push } from 'aemsync' -const result = (await push(args).next()).value -console.log(result) +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' + ] +} + +// Will yield for each target. +for await (const result of push(args)) { + console.log(result) +} ``` -JavaScript's arguments and defaults for `aemsync()` and `push()` functions: +Defaults for `args`: -```JavaScript +```js const args = { workingDir: '.', - exclude: ['**/jcr_root/*', '**/@(.git|.svn|.hg|target)', '**/@(.git|.svn|.hg|target)/**'], + exclude: [ + // AEM root folders (we do not want to accidentally delete them). + '**/jcr_root/*', + // Special files. + '**/@(.*|target|[Tt]humbs.db|[Dd]esktop.ini)', + // Special folders. + '**/@(.*|target)/**' + ], packmgrPath: '/crx/packmgr/service.jsp', - targets: ['http://admin:admin@localhost:4501'], + targets: ['http://admin:admin@localhost:4502'], delay: 300, - checkIfUp: false + checkIfUp: false, + verbose: false } ``` -### Description +## Description Watching for file changes is fast, since it uses Node's `recursive` option for `fs.watch()` where applicable. @@ -108,7 +128,7 @@ Any changes inside `jcr_root` folders are detected and uploaded to AEM instance( The delay is the time elapsed 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; 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 it at the default. -### Caveats +## Caveats 1. Packages are installed using the package manager service (`/crx/packmgr/service.jsp`), which takes some time to initialize after AEM startup. If the push happens before initialization, the Sling Post Servlet will take over, causing the `/crx/packmgr/service.jsp/file` node to be added to the repository. Use the `-c` option to perform 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 handling is left to the package manager. diff --git a/bin/aemsync.js b/bin/aemsync.js deleted file mode 100644 index 7c4dbd9..0000000 --- a/bin/aemsync.js +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env node -import { main } from '../src/index.js' -main() diff --git a/package-lock.json b/package-lock.json index 7d0ca15..54a811f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,10 @@ "xml-to-json-stream": "^1.1.0" }, "bin": { - "aemsync": "bin/aemsync.js" + "aemsync": "src/cli.js" }, "devDependencies": { - "dotenv": "^17.2.1", + "dotenv": "^17.2.2", "dotenv-cli": "^10.0.0", "fs-extra": "^11.3.1", "release-it": "^19.0.4", @@ -1596,9 +1596,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", - "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", "dev": true, "license": "BSD-2-Clause", "engines": { diff --git a/package.json b/package.json index 08cc942..d6ae45b 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "type": "git", "url": "https://github.com/gavoja/aemsync.git" }, - "main": "src/index.js", + "main": "src/api.js", "devDependencies": { - "dotenv": "^17.2.1", + "dotenv": "^17.2.2", "dotenv-cli": "^10.0.0", "fs-extra": "^11.3.1", "release-it": "^19.0.4", @@ -33,7 +33,7 @@ "xml-to-json-stream": "^1.1.0" }, "scripts": { - "start": "node ./bin/aemsync.js", + "start": "node ./src/cli.js", "lint": "npx standard", "format": "npx standard --fix", "test": "node --test --test-reporter=spec --test-reporter-destination=stdout --experimental-test-coverage --test-coverage-exclude \"test/**/*\"", @@ -42,10 +42,10 @@ "release:dry": "dotenv release-it --dry-run" }, "engines": { - "node": ">=16.16.0" + "node": ">=22.18.0" }, "bin": { - "aemsync": "./bin/aemsync.js" + "aemsync": "./src/cli.js" }, "license": "MIT", "false": {} diff --git a/src/index.js b/src/api.js similarity index 54% rename from src/index.js rename to src/api.js index 967fb1f..6abff39 100644 --- a/src/index.js +++ b/src/api.js @@ -1,16 +1,12 @@ -import fs from 'fs' -import path from 'path' -import * as url from 'url' +import fs from 'node:fs' import xmlToJson from 'xml-to-json-stream' import * as log from './log.js' import Package from './package.js' import watch from './watch.js' const ZIP_RETRY_DELAY = 3000 -const DIRNAME = url.fileURLToPath(new URL('.', import.meta.url)) -const PACKAGE_JSON = path.resolve(DIRNAME, '..', 'package.json') -const VERSION = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf8')).version -const DEFAULTS = { + +export const DEFAULTS = { workingDir: '.', exclude: [ // AEM root folders (we do not want to accidentally delete them). @@ -28,49 +24,8 @@ const DEFAULTS = { verbose: false } -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/* - **/@(.*|target|[Tt]humbs.db|[Dd]esktop.ini) - **/@(.*|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 **/test/** - Just push, don't watch: - > aemsync -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component - Push multiple: - > aemsync -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-other-component - -Website: - https://github.com/gavoja/aemsync -` - // ============================================================================= -// Posting to AEM. +// Helper functions. // ============================================================================= async function post ({ archivePath, target, packmgrPath, checkIfUp }) { @@ -152,14 +107,14 @@ function parseXml (xml) { }) } -// ============================================================================= -// Main API. -// ============================================================================= - async function wait (ms) { return new Promise(resolve => setTimeout(resolve, ms)) } +// ============================================================================= +// Main API. +// ============================================================================= + export async function * push (args) { const { payload, exclude, targets, packmgrPath, checkIfUp, postHandler, breakStuff } = { ...DEFAULTS, ...args } @@ -206,90 +161,3 @@ export async function * aemsync (args) { } } } - -// ============================================================================= -// 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 = [' ', ...process.argv.slice(2)].join(' ').split(' -').slice(1).reduce((obj, arg) => { - const [key, value] = arg.split(/ (.*)/s) - obj[key] = obj[key] ?? [] - obj[key].push(value) - return obj - }, {}) - - return { - payload: args.p ? args.p.map(p => path.resolve(p)) : null, - workingDir: path.resolve(args?.w?.[0] ?? DEFAULTS.workingDir), - targets: args.t ?? DEFAULTS.targets, - exclude: args.e ?? DEFAULTS.exclude, - delay: Number(args?.d?.[0]) || DEFAULTS.delay, - checkIfUp: !!args.c, - packmgrPath: args?.q?.pop?.() ?? DEFAULTS.packmgrPath, - help: !!args.h, - verbose: !!args.v - } -} - -export 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) { - const result = (await push(args).next()).value - debugResult(result) - return - } - - // - // 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/src/cli.js b/src/cli.js new file mode 100644 index 0000000..70975e3 --- /dev/null +++ b/src/cli.js @@ -0,0 +1,131 @@ +#!/usr/bin/env node +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { DEFAULTS, aemsync, push } from './api.js' +import * as log from './log.js' + +const DIRNAME = fileURLToPath(new URL('.', import.meta.url)) +const PACKAGE_JSON = path.resolve(DIRNAME, '..', 'package.json') +const VERSION = JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf8')).version +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/* + **/@(.*|target|[Tt]humbs.db|[Dd]esktop.ini) + **/@(.*|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 **/test/** + Just push, don't watch: + > aemsync -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component + Push multiple: + > aemsync -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-other-component + +Website: + https://github.com/gavoja/aemsync +` + +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 = [' ', ...process.argv.slice(2)].join(' ').split(' -').slice(1).reduce((obj, arg) => { + const [key, value] = arg.split(/ (.*)/s) + obj[key] = obj[key] ?? [] + obj[key].push(value) + return obj + }, {}) + + return { + payload: args.p ? args.p.map(p => path.resolve(p)) : null, + workingDir: path.resolve(args?.w?.[0] ?? DEFAULTS.workingDir), + targets: args.t ?? DEFAULTS.targets, + exclude: args.e ?? DEFAULTS.exclude, + delay: Number(args?.d?.[0]) || DEFAULTS.delay, + checkIfUp: !!args.c, + packmgrPath: args?.q?.pop?.() ?? DEFAULTS.packmgrPath, + help: !!args.h, + verbose: !!args.v + } +} + +export async function main () { + const args = getArgs() + + // Show help. + if (args.help) { + log.info(HELP) + return + } + + // Print additional debug information. + args.verbose && log.enableDebug() + + // + // Just push. + // + + // Path to push does not have to exist. + // Non-existing path can be used for deletion. + if (args.payload) { + const result = (await push(args).next()).value + debugResult(result) + return + } + + // + // 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) + } +} + +main() diff --git a/src/package.js b/src/package.js index f210d75..487399b 100644 --- a/src/package.js +++ b/src/package.js @@ -1,12 +1,12 @@ -import fs from 'fs' import globrex from 'globrex' -import path from 'path' -import * as url from 'url' -import util from 'util' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import util from 'node:util' import * as log from './log.js' import Zip from './zip.js' -const DIRNAME = url.fileURLToPath(new URL('..', import.meta.url)) +const DIRNAME = fileURLToPath(new URL('..', import.meta.url)) const DATA_PATH = path.resolve(DIRNAME, 'data') const PACKAGE_CONTENT_PATH = path.join(DATA_PATH, 'package-content') const NT_FOLDER_PATH = path.join(DATA_PATH, 'nt-folder', '.content.xml') diff --git a/src/zip.js b/src/zip.js index 785b0de..746246a 100644 --- a/src/zip.js +++ b/src/zip.js @@ -1,7 +1,7 @@ import AdmZip from 'adm-zip' -import fs from 'fs' -import os from 'os' -import path from 'path' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' const DEFAULT_ARCHIVE_PATH = path.join(os.tmpdir(), 'aemsync.zip') diff --git a/test/cases/aemsync.test.js b/test/cases/aemsync.test.js index 2e3542b..7d103d4 100644 --- a/test/cases/aemsync.test.js +++ b/test/cases/aemsync.test.js @@ -2,7 +2,7 @@ import fs from 'fs-extra' import assert from 'node:assert' import path from 'node:path' import { after, before, test } from 'node:test' -import { aemsync } from '../../src/index.js' +import { aemsync } from '../../src/api.js' const SAMPLE_CONTENT = path.resolve('./test/data') const TEMP = path.resolve('./temp')