meca 0.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ import type { ISession, JatsOptions } from 'jats-xml';
2
+ /**
3
+ * Validate a given file as MECA bundle
4
+ *
5
+ * Returns true if file is valid.
6
+ *
7
+ * Validation checks:
8
+ * - File exists and is zip format
9
+ * - Bundle includes manifest.xlm which validates against DTD
10
+ * - manifest matches items present in the bundle
11
+ * - manifest item types match known types
12
+ * - JATS items validate
13
+ */
14
+ export declare function validateMeca(session: ISession, file: string, opts: Partial<JatsOptions>): Promise<boolean>;
15
+ /**
16
+ * Validate a given file as MECA bundle
17
+ *
18
+ * Logs confirmation message if valid and throws an error if invalid.
19
+ */
20
+ export declare function validateMecaWrapper(session: ISession, file: string, opts: Partial<JatsOptions>): Promise<void>;
@@ -0,0 +1,166 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import AdmZip from 'adm-zip';
5
+ import chalk from 'chalk';
6
+ import { xml2js } from 'xml-js';
7
+ import { validateJatsAgainstDtd, xmllintValidate } from 'jats-xml';
8
+ const KNOWN_ITEM_TYPES = [
9
+ 'article-metadata',
10
+ 'article-supporting-file',
11
+ 'manuscript',
12
+ 'manuscript-supporting-file',
13
+ 'article-source',
14
+ 'article-source-environment',
15
+ 'article-source-directory',
16
+ ];
17
+ const MANIFEST = 'manifest.xml';
18
+ const MANIFEST_DTD = 'MECA_manifest.dtd';
19
+ /**
20
+ * Function to log debug message for passing check
21
+ */
22
+ function debugCheck(session, msg) {
23
+ session.log.debug(chalk.green(`✓ ${msg}`));
24
+ }
25
+ /**
26
+ * Function to log an error and clean temp folder
27
+ */
28
+ function errorAndClean(session, msg, tempFolder) {
29
+ session.log.error(msg);
30
+ removeTempFolder(tempFolder);
31
+ return false;
32
+ }
33
+ function removeTempFolder(tempFolder) {
34
+ if (tempFolder && fs.existsSync(tempFolder)) {
35
+ if (fs.rmSync) {
36
+ // Node >= 14.14
37
+ fs.rmSync(tempFolder, { recursive: true });
38
+ }
39
+ else {
40
+ // Node < 14.14
41
+ fs.rmdirSync(tempFolder, { recursive: true });
42
+ }
43
+ }
44
+ }
45
+ function createTempFolder() {
46
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'meca'));
47
+ }
48
+ /**
49
+ * Extract list of simple manifest item from valid manifest XML
50
+ */
51
+ function extractManifestItems(manifestString) {
52
+ var _a, _b, _c, _d, _e;
53
+ const manifest = xml2js(manifestString);
54
+ const items = (_e = (_d = (_c = (_b = (_a = manifest.elements) === null || _a === void 0 ? void 0 : _a.find((element) => element.name === 'manifest')) === null || _b === void 0 ? void 0 : _b.elements) === null || _c === void 0 ? void 0 : _c.filter((element) => element.name === 'item')) === null || _d === void 0 ? void 0 : _d.map((item) => {
55
+ var _a, _b, _c;
56
+ const instanceAttrs = (_b = (_a = item.elements) === null || _a === void 0 ? void 0 : _a.find((element) => element.name === 'instance')) === null || _b === void 0 ? void 0 : _b.attributes;
57
+ const href = instanceAttrs === null || instanceAttrs === void 0 ? void 0 : instanceAttrs['xlink:href'];
58
+ if (!href)
59
+ return undefined;
60
+ return {
61
+ href,
62
+ itemType: (_c = item.attributes) === null || _c === void 0 ? void 0 : _c['item-type'],
63
+ mediaType: instanceAttrs['media-type'],
64
+ };
65
+ }).filter((item) => !!item)) !== null && _e !== void 0 ? _e : [];
66
+ return items;
67
+ }
68
+ /**
69
+ * Validate a given file as MECA bundle
70
+ *
71
+ * Returns true if file is valid.
72
+ *
73
+ * Validation checks:
74
+ * - File exists and is zip format
75
+ * - Bundle includes manifest.xlm which validates against DTD
76
+ * - manifest matches items present in the bundle
77
+ * - manifest item types match known types
78
+ * - JATS items validate
79
+ */
80
+ export async function validateMeca(session, file, opts) {
81
+ if (!fs.existsSync(file))
82
+ return errorAndClean(session, `Input file does not exists: ${file}`);
83
+ let mecaZip;
84
+ try {
85
+ mecaZip = new AdmZip(file);
86
+ }
87
+ catch {
88
+ return errorAndClean(session, `Input file is not a zip archive: ${file}`);
89
+ }
90
+ debugCheck(session, 'is zip archive');
91
+ const manifestEntry = mecaZip.getEntry(MANIFEST);
92
+ if (!manifestEntry) {
93
+ return errorAndClean(session, `Input zip archive does not include required manifest file '${MANIFEST}'`);
94
+ }
95
+ debugCheck(session, `includes ${MANIFEST}`);
96
+ const localDtdFile = fs.existsSync(path.join(__dirname, MANIFEST_DTD))
97
+ ? path.join(__dirname, MANIFEST_DTD)
98
+ : path.join(__dirname, '..', MANIFEST_DTD);
99
+ if (!fs.existsSync(localDtdFile)) {
100
+ throw new Error(`Unable to locate manifest DTD file ${MANIFEST_DTD} in meca lib distribution`);
101
+ }
102
+ const tempFolder = createTempFolder();
103
+ mecaZip.extractEntryTo(MANIFEST, tempFolder);
104
+ const manifestIsValid = await xmllintValidate(session, path.join(tempFolder, MANIFEST), localDtdFile);
105
+ if (!manifestIsValid) {
106
+ return errorAndClean(session, `${MANIFEST} DTD validation failed`, tempFolder);
107
+ }
108
+ debugCheck(session, `${MANIFEST} passes schema validation`);
109
+ const manifestString = manifestEntry.getData().toString();
110
+ const manifestItems = extractManifestItems(manifestString);
111
+ const zipEntries = mecaZip.getEntries();
112
+ const manifestExtras = manifestItems
113
+ .filter((item) => !zipEntries.map((entry) => entry.entryName).includes(item.href))
114
+ .map((item) => item.href);
115
+ const zipExtras = zipEntries
116
+ .filter((entry) => entry.entryName !== MANIFEST)
117
+ .filter((entry) => !entry.isDirectory)
118
+ .filter((entry) => !manifestItems.map((item) => item.href).includes(entry.entryName))
119
+ .map((entry) => entry.entryName);
120
+ if (zipExtras.length) {
121
+ session.log.warn(`MECA bundle includes items missing from manifest:\n- ${zipExtras.join('\n- ')}`);
122
+ }
123
+ if (manifestExtras.length) {
124
+ return errorAndClean(session, `manifest items missing from MECA bundle:\n- ${manifestExtras.join('\n- ')}`, tempFolder);
125
+ }
126
+ debugCheck(session, 'manfiest matches MECA bundle contents');
127
+ manifestItems.forEach((item) => {
128
+ if (!item.mediaType) {
129
+ session.log.warn(`manifest item missing media-type: ${item.href}`);
130
+ }
131
+ if (!item.itemType) {
132
+ session.log.warn(`manifest item missing item-type: ${item.href} `);
133
+ }
134
+ else if (!KNOWN_ITEM_TYPES.includes(item.itemType)) {
135
+ session.log.warn(`manifest item has unknown item-type "${item.itemType}": ${item.href} `);
136
+ }
137
+ });
138
+ const jatsFiles = manifestItems
139
+ .filter((item) => item.itemType === 'article-metadata')
140
+ .map((item) => item.href);
141
+ const invalidJatsFiles = (await Promise.all(jatsFiles.map(async (jatsFile) => {
142
+ mecaZip.extractEntryTo(jatsFile, tempFolder);
143
+ const isValid = await validateJatsAgainstDtd(session, path.join(tempFolder, ...jatsFile.split('/')), opts);
144
+ return isValid ? undefined : jatsFile;
145
+ }))).filter((jatsFile) => !!jatsFile);
146
+ if (invalidJatsFiles.length) {
147
+ return errorAndClean(session, `JATS DTD validation failed:\n- ${invalidJatsFiles.join('\n- ')}`, tempFolder);
148
+ }
149
+ debugCheck(session, 'JATS validation passed');
150
+ removeTempFolder(tempFolder);
151
+ return true;
152
+ }
153
+ /**
154
+ * Validate a given file as MECA bundle
155
+ *
156
+ * Logs confirmation message if valid and throws an error if invalid.
157
+ */
158
+ export async function validateMecaWrapper(session, file, opts) {
159
+ const success = await validateMeca(session, file, opts);
160
+ if (success) {
161
+ session.log.info(chalk.greenBright('MECA validation passed!'));
162
+ }
163
+ else {
164
+ throw new Error('MECA validation failed.');
165
+ }
166
+ }
@@ -0,0 +1 @@
1
+ export * from './dtd.js';
@@ -0,0 +1 @@
1
+ export * from './dtd.js';
@@ -0,0 +1,2 @@
1
+ declare const version = "1.0.3";
2
+ export default version;
@@ -0,0 +1,2 @@
1
+ const version = '1.0.3';
2
+ export default version;
package/package.json CHANGED
@@ -1,11 +1,63 @@
1
1
  {
2
2
  "name": "meca",
3
- "version": "0.0.0",
4
- "description": "Manuscript Exchange Common Approach",
5
- "main": "index.js",
3
+ "version": "1.0.3",
4
+ "description": "Types and utilities for working with MECA",
5
+ "author": "Rowan Cockett <rowan@curvenote.com>",
6
+ "homepage": "https://github.com/curvenote/meca",
7
+ "license": "MIT",
8
+ "sideEffects": false,
9
+ "type": "module",
10
+ "exports": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "keywords": [
16
+ "jats",
17
+ "open-science",
18
+ "publishing"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/curvenote/meca.git"
26
+ },
27
+ "bin": "./dist/meca.cjs",
6
28
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
29
+ "copy:dtd": "cp ./static/MECA_manifest.dtd dist/MECA_manifest.dtd",
30
+ "copy:version": "echo \"const version = '\"$npm_package_version\"';\nexport default version;\" > src/version.ts",
31
+ "clean": "rm -rf dist",
32
+ "unlink": "npm uninstall -g meca;",
33
+ "link": "npm run unlink; npm link;",
34
+ "dev": "npm run copy:version && npm run link && esbuild src/cli/index.ts --bundle --outfile=dist/meca.cjs --platform=node --watch && npm run copy:dtd",
35
+ "test": "npm run copy:version && vitest run",
36
+ "test:watch": "npm run copy:version && vitest watch",
37
+ "lint": "eslint \"src/**/*.ts*\" -c ./.eslintrc.cjs",
38
+ "lint:format": "prettier --check \"src/**/*.{ts,tsx,md}\"",
39
+ "build:esm": "tsc --project ./tsconfig.json --module es2015 --outDir dist --declaration",
40
+ "build:cli": "esbuild src/cli/index.ts --bundle --outfile=dist/meca.cjs --platform=node",
41
+ "build": "npm-run-all -l clean copy:version -p build:esm build:cli -s copy:dtd"
8
42
  },
9
- "author": "Rowan Cockett <rowan@curvenote.com>",
10
- "license": "MIT"
43
+ "bugs": {
44
+ "url": "https://github.com/curvenote/meca/issues"
45
+ },
46
+ "dependencies": {
47
+ "adm-zip": "^0.5.10",
48
+ "jats-xml": "^1.0.3",
49
+ "xml-js": "^1.6.11"
50
+ },
51
+ "peerDependencies": {
52
+ "chalk": "^5.2.0",
53
+ "commander": "^10.0.1"
54
+ },
55
+ "devDependencies": {
56
+ "@types/adm-zip": "^0.5.0",
57
+ "chalk": "^5.2.0",
58
+ "commander": "^10.0.1",
59
+ "myst-cli-utils": "^1.0.0",
60
+ "myst-common": "^1.0.0",
61
+ "myst-frontmatter": "^1.0.0"
62
+ }
11
63
  }