Skip to content

Commit 375bd71

Browse files
committed
Force POSIX paths in extracted adventure entries. Add additional volatile fields. Ignore volatile fields on embedded children.
1 parent 8065cbe commit 375bd71

4 files changed

Lines changed: 74 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## 3.0.0
2+
3+
### Breaking Changes
4+
- (Jeff Hitchcock) Using the `--expandAdventures` option without `--folders` now adds the document type to the extracted entry's name to disambiguate it in cases where it shares a name and ID with another document in the adventure of a different type.
5+
- File paths inside extracted adventure entries now always use POSIX delimiters, regardless of which OS the extraction occurs on.
6+
7+
### Improvements
8+
- Added `_stats.coreVersion` and `_stats.systemVersion` to the list of volatile fields ignored by the `--omitVolatile` flag.
9+
- Volatile fields are now also ignored on embedded children.
10+
111
## 2.0.0
212

313
### Fixes

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ Extract the contents of a compendium pack into individual source files for each
214214
* **transformName:** *(entry: object): Promise<string|void>* A function that is called on every entry. The value returned from this will be used as the entry's filename and must include the appropriate file extension. If nothing is returned, an auto-generated name will be used instead.
215215
* **transformFolderName:** *(entry: object): Promise<string|void>* A function used to generate a directory name for an extracted Folder document when the `folders` option is used.
216216
* **expandAdventures:** *boolean* Write documents embedded in Adventures to their own files. If the `folders` option is also supplied, the Adventure is treated like a folder, and written to `_Adventure.{yml|json}` instead of `_Folder.{yml|json}`. Additionally, all its entries are grouped into sub-folders by Document type.
217-
* **omitVolatile:** *boolean* When unpacking, diff the candidate entry against an existing one and only write it if non-volatile fields have changed. Currently, `_stats.createdTime`, `_stats.modifiedTime`, and `_stats.lastModifiedBy` are considered volatile.
217+
* **omitVolatile:** *boolean* When unpacking, diff the candidate entry against an existing one and only write it if non-volatile fields have changed. Currently, `_stats.createdTime`, `_stats.modifiedTime`, `_stats.lastModifiedBy`, `_stats.systemVersion`, and `_stats.coreVersion` are considered volatile.
218218
* **jsonOptions:** *object*
219219
* **replacer:** *(key: string, value: any): any|Array<string|number>* A replacer function or an array of property names in the object to include in the resulting string.
220220
* **space:** *string|number* A number of spaces or a string to use as indentation.

commands/package.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ import { compilePack, extractPack, TYPE_COLLECTION_MAP } from "../lib/package.mj
3232
* each contained document as its own entry in a folder.
3333
* @property {boolean} [omitVolatile] When unpacking, diff the candidate entry against an existing one
3434
* and only write it if non-volatile fields have changed.
35-
* Currently, _stats.createdTime, _stats.modifiedTime, and
36-
* _stats.lastModifiedBy are considered volatile.
35+
* Currently, _stats.createdTime, _stats.modifiedTime,
36+
* _stats.lastModifiedBy, _stats.systemVersion, and
37+
* _stats.coreVersion are considered volatile.
3738
*/
3839

3940
/**
@@ -144,7 +145,7 @@ export function getCommand() {
144145
});
145146

146147
yargs.option("omitVolatile", {
147-
describe: "When unpacking, diff the candidate entry against an existing one and only write it if non-volatile fields have changed. Currently, _stats.createdTime, _stats.modifiedTime, and _stats.lastModifiedBy are considered volatile.",
148+
describe: "When unpacking, diff the candidate entry against an existing one and only write it if non-volatile fields have changed. Currently, _stats.createdTime, _stats.modifiedTime, _stats.lastModifiedBy, _stats.systemVersion, and _stats.coreVersion are considered volatile.",
148149
type: "boolean"
149150
});
150151

lib/package.mjs

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ import { ClassicLevel } from "classic-level";
5353
* Document type.
5454
* @property {boolean} [omitVolatile] Do not overwrite an existing entry if the new one has changes to
5555
* non-volatile fields. Currently, _stats.createdTime,
56-
* _stats.modifiedTime, and _stats.lastModifiedBy are considered
57-
* volatile.
56+
* _stats.modifiedTime, _stats.lastModifiedBy, _stats.systemVersion,
57+
* and _stats.coreVersion are considered volatile.
5858
* @property {DocumentCollection} [collection] Required only for NeDB packs in order to generate a correct key.
5959
* Can be used instead of documentType if known.
6060
* @property {NameTransformer} [transformName] A function that is used to generate a filename for the extracted
@@ -117,6 +117,15 @@ import { ClassicLevel } from "classic-level";
117117
* @returns {Promise<object|void>} Options to supply to the next level of the hierarchy.
118118
*/
119119

120+
/**
121+
* @callback HierarchyApplySyncCallback
122+
* @param {object} doc The Document being operated on.
123+
* @param {string} collection The Document's collection.
124+
* @param {number|null} i The Document's index in its parent collection.
125+
* @param {object} [options] Additional options supplied by the invocation on the level above this one.
126+
* @returns {object|void}
127+
*/
128+
120129
/**
121130
* @callback HierarchyMapCallback
122131
* @param {any} entry The element stored in the collection.
@@ -213,6 +222,12 @@ export const TYPE_COLLECTION_MAP = {
213222
*/
214223
export const COLLECTION_TYPE_MAP = Object.fromEntries(Object.entries(TYPE_COLLECTION_MAP).map(([k, v]) => [v, k]));
215224

225+
/**
226+
* When extracting an entry, if the newly-extracted entry has only these fields changed, then ignore it.
227+
* @type {string[]}
228+
*/
229+
const VOLATILE_FIELDS = ["createdTime", "modifiedTime", "lastModifiedBy", "systemVersion", "coreVersion"];
230+
216231
/* -------------------------------------------- */
217232
/* Compiling */
218233
/* -------------------------------------------- */
@@ -489,7 +504,7 @@ async function extractNedb(pack, dest, {
489504
name = `${doc.name ? `${getSafeFilename(doc.name)}_${doc._id}` : doc._id}.${yaml ? "yml" : "json"}`;
490505
}
491506
const filename = path.join(dest, name);
492-
serializeDocument(checkVolatile(doc, name, { omitVolatile, existing, yaml }), filename, {
507+
serializeDocument(checkVolatile(doc, name, { collection, omitVolatile, existing, yaml }), filename, {
493508
yaml, yamlOptions, jsonOptions
494509
});
495510
if ( log ) console.log(`Wrote ${chalk.blue(name)}`);
@@ -554,7 +569,7 @@ async function extractClassicLevel(pack, dest, {
554569
if ( folder ) name = path.join(folder, name);
555570
}
556571
const filename = path.join(dest, name);
557-
serializeDocument(checkVolatile(doc, name, { omitVolatile, existing, yaml }), filename, {
572+
serializeDocument(checkVolatile(doc, name, { collection, omitVolatile, existing, yaml }), filename, {
558573
yaml, yamlOptions, jsonOptions
559574
});
560575
if ( log ) console.log(`Wrote ${chalk.blue(name)}`);
@@ -617,8 +632,13 @@ async function extractAdventure(doc, dest, { folderMap }={}, {
617632
if ( embeddedFolder ) embeddedName = path.join(embeddedFolder, embeddedName);
618633
}
619634
const filename = path.join(dest, embeddedName);
620-
paths.push(adventureFolder ? path.relative(adventureFolder, embeddedName) : path.basename(embeddedName));
621-
serializeDocument(checkVolatile(embeddedDoc, embeddedName, { omitVolatile, existing, yaml }), filename, {
635+
const embeddedPath =
636+
adventureFolder ? path.relative(adventureFolder, embeddedName) : path.basename(embeddedName);
637+
paths.push(path.posix.join(...embeddedPath.split(path.sep)));
638+
serializeDocument(checkVolatile(embeddedDoc, embeddedName, {
639+
omitVolatile, existing, yaml,
640+
collection: embeddedCollectionName
641+
}), filename, {
622642
yaml, yamlOptions, jsonOptions
623643
});
624644
if ( log ) console.log(`Wrote ${chalk.blue(embeddedName)}`);
@@ -659,6 +679,28 @@ function applyHierarchy(fn) {
659679

660680
/* -------------------------------------------- */
661681

682+
/**
683+
* Wrap a synchronous function so that it can be applied recursively to a Document's hierarchy.
684+
* @param {HierarchyApplySyncCallback} fn The function to wrap.
685+
* @returns {HierarchyApplySyncCallback} The wrapped function.
686+
*/
687+
function applyHierarchySync(fn) {
688+
return function apply(doc, collection, i, options={}) {
689+
const newOptions = fn(doc, collection, i, options);
690+
for ( const [embeddedCollectionName, type] of Object.entries(HIERARCHY[collection] ?? {}) ) {
691+
const embeddedValue = doc[embeddedCollectionName];
692+
if ( Array.isArray(type) && Array.isArray(embeddedValue) ) {
693+
for ( let i = 0; i < embeddedValue.length; i++ ) {
694+
apply(embeddedValue[i], embeddedCollectionName, i, newOptions);
695+
}
696+
}
697+
else if ( embeddedValue ) apply(embeddedValue, embeddedCollectionName, -1, newOptions);
698+
}
699+
};
700+
}
701+
702+
/* -------------------------------------------- */
703+
662704
/**
663705
* Build up the folder structure used to extract files.
664706
* @param {object[]} folders Folders to process.
@@ -765,18 +807,25 @@ function serializeDocument(doc, filename, { yaml, yamlOptions, jsonOptions }={})
765807
* @param {boolean} [options.omitVolatile] Only write changes if some non-volatile fields have changed.
766808
* @param {string} [options.existing] The location of the existing file.
767809
* @param {boolean} [options.yaml] Whether the existing files are formatted with YAML.
810+
* @param {string} [options.collection] The primaru Document collection, in order to perform hierarchical
811+
* operations.
768812
* @returns {object} The data to write. Either the candidate data, or the existing data.
769813
*/
770-
function checkVolatile(doc, name, { omitVolatile, existing, yaml }={}) {
814+
function checkVolatile(doc, name, { omitVolatile, existing, yaml, collection }={}) {
771815
if ( !omitVolatile || !existing || !("_stats" in doc) ) return doc;
772816
const parse = yaml ? YAML.load : JSON.parse;
773817
try {
774818
const base = parse(fs.readFileSync(path.join(existing, name), { encoding: "utf8" }));
775819
if ( !base || !("_stats" in base) ) return doc;
776820
const copy = structuredClone(doc);
777-
if ( "createdTime" in base._stats ) copy._stats.createdTime = base._stats.createdTime;
778-
if ( "modifiedTime" in base._stats ) copy._stats.modifiedTime = base._stats.modifiedTime;
779-
if ( "lastModifiedBy" in base._stats ) copy._stats.lastModifiedBy = base._stats.lastModifiedBy;
821+
const apply = applyHierarchySync((a, collection, i) => {
822+
const b = i === null ? base : i < 0 ? base[collection] : base[collection]?.[i];
823+
if ( !b || !("_stats" in b) ) return;
824+
for ( const p of VOLATILE_FIELDS ) {
825+
if ( p in b._stats ) a._stats[p] = b._stats[p];
826+
}
827+
});
828+
apply(copy, collection, null);
780829
return testEquality(base, copy) ? base : doc;
781830
} catch {
782831
return doc;

0 commit comments

Comments
 (0)