Skip to content

Commit 635404a

Browse files
authored
feat(@jsii/utils): add a utility library to be consumed by jsii modules (#3570)
Adds a set of utility functions under `@jsii/spec` that can (and will) be leveraged by other jsii modules. Currently has the following functions exposed: - `getAssemblyFile()` - `writeAssembly()` - `loadAssemblyFromPath()` - `loadAssemblyFromFile()` --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
1 parent 5b8466a commit 635404a

File tree

6 files changed

+310
-7
lines changed

6 files changed

+310
-7
lines changed

packages/@jsii/spec/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"package": "package-js"
3232
},
3333
"dependencies": {
34-
"ajv": "^8.11.0"
34+
"ajv": "^8.11.0",
35+
"fs-extra": "^10.1.0"
3536
},
3637
"devDependencies": {
3738
"jsii-build-tools": "^0.0.0",
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import * as fs from 'fs-extra';
2+
import * as os from 'os';
3+
import * as path from 'path';
4+
5+
import {
6+
SPEC_FILE_NAME,
7+
SPEC_FILE_NAME_COMPRESSED,
8+
Assembly,
9+
SchemaVersion,
10+
} from './assembly';
11+
import {
12+
loadAssemblyFromPath,
13+
getAssemblyFile,
14+
writeAssembly,
15+
} from './assembly-utils';
16+
17+
const TEST_ASSEMBLY: Assembly = {
18+
schema: SchemaVersion.LATEST,
19+
name: 'jsii-test-dep',
20+
version: '1.2.4',
21+
license: 'Apache-2.0',
22+
description: 'A test assembly',
23+
homepage: 'https://github.com/aws/jsii',
24+
repository: { type: 'git', url: 'git://github.com/aws/jsii.git' },
25+
author: {
26+
name: 'Amazon Web Services',
27+
url: 'https://aws.amazon.com',
28+
organization: true,
29+
roles: ['author'],
30+
},
31+
fingerprint: 'F1NG3RPR1N7',
32+
dependencies: {
33+
'jsii-test-dep-dep': '3.2.1',
34+
},
35+
jsiiVersion: '1.0.0',
36+
};
37+
38+
describe('writeAssembly', () => {
39+
test('can write compressed assembly', () => {
40+
const tmpdir = makeTempDir();
41+
writeAssembly(tmpdir, TEST_ASSEMBLY, { compress: true });
42+
43+
expect(
44+
fs.existsSync(path.join(tmpdir, SPEC_FILE_NAME_COMPRESSED)),
45+
).toBeTruthy();
46+
47+
// includes .jsii files with instructions for finding compressed file
48+
const instructions = fs.readJsonSync(path.join(tmpdir, SPEC_FILE_NAME), {
49+
encoding: 'utf-8',
50+
});
51+
expect(instructions).toEqual({
52+
schema: 'jsii/file-redirect',
53+
compression: 'gzip',
54+
filename: SPEC_FILE_NAME_COMPRESSED,
55+
});
56+
});
57+
58+
test('can write uncompressed assembly', () => {
59+
const tmpdir = makeTempDir();
60+
writeAssembly(tmpdir, TEST_ASSEMBLY, { compress: false });
61+
62+
expect(fs.existsSync(path.join(tmpdir, SPEC_FILE_NAME))).toBeTruthy();
63+
});
64+
});
65+
66+
describe('getAssemblyFile', () => {
67+
test('finds SPEC_FILE_NAME file when there is no compression', () => {
68+
const tmpdir = makeTempDir();
69+
writeAssembly(tmpdir, TEST_ASSEMBLY, { compress: false });
70+
71+
expect(getAssemblyFile(tmpdir)).toEqual(path.join(tmpdir, SPEC_FILE_NAME));
72+
});
73+
74+
test('finds SPEC_FILE_NAME file even when there is compression', () => {
75+
const tmpdir = makeTempDir();
76+
writeAssembly(tmpdir, TEST_ASSEMBLY, { compress: true });
77+
78+
expect(getAssemblyFile(tmpdir)).toEqual(path.join(tmpdir, SPEC_FILE_NAME));
79+
});
80+
81+
test('throws if SPEC_FILE_NAME file does not exist', () => {
82+
const tmpdir = makeTempDir();
83+
84+
expect(() => getAssemblyFile(tmpdir)).toThrow(
85+
`Expected to find ${SPEC_FILE_NAME} file in ${tmpdir}, but no such file found`,
86+
);
87+
});
88+
});
89+
90+
describe('loadAssemblyFromPath', () => {
91+
test('loads compressed assembly', () => {
92+
const tmpdir = makeTempDir();
93+
writeAssembly(tmpdir, TEST_ASSEMBLY, { compress: true });
94+
95+
expect(loadAssemblyFromPath(tmpdir)).toEqual(TEST_ASSEMBLY);
96+
});
97+
98+
test('loads uncompressed assembly', () => {
99+
const tmpdir = makeTempDir();
100+
writeAssembly(tmpdir, TEST_ASSEMBLY, { compress: false });
101+
102+
expect(loadAssemblyFromPath(tmpdir)).toEqual(TEST_ASSEMBLY);
103+
});
104+
105+
test('compressed and uncompressed assemblies are loaded identically', () => {
106+
const compressedTmpDir = makeTempDir();
107+
const uncompressedTmpDir = makeTempDir();
108+
109+
writeAssembly(compressedTmpDir, TEST_ASSEMBLY, { compress: true });
110+
writeAssembly(uncompressedTmpDir, TEST_ASSEMBLY, { compress: false });
111+
112+
expect(loadAssemblyFromPath(compressedTmpDir)).toEqual(
113+
loadAssemblyFromPath(uncompressedTmpDir),
114+
);
115+
});
116+
117+
test('throws if redirect schema is invalid', () => {
118+
const tmpdir = makeTempDir();
119+
fs.writeJsonSync(path.join(tmpdir, SPEC_FILE_NAME), {
120+
schema: 'jsii/file-redirect',
121+
compression: '7zip',
122+
});
123+
124+
expect(() => loadAssemblyFromPath(tmpdir)).toThrow(
125+
[
126+
'Invalid redirect schema:',
127+
" compression must be 'gzip' but received '7zip'",
128+
" schema must include property 'filename'",
129+
].join('\n'),
130+
);
131+
});
132+
133+
test('throws if assembly is invalid', () => {
134+
const tmpdir = makeTempDir();
135+
fs.writeJsonSync(
136+
path.join(tmpdir, SPEC_FILE_NAME),
137+
{
138+
assembly: 'not a valid assembly',
139+
},
140+
{
141+
encoding: 'utf8',
142+
spaces: 2,
143+
},
144+
);
145+
146+
expect(() => loadAssemblyFromPath(tmpdir)).toThrow(/Invalid assembly/);
147+
});
148+
});
149+
150+
function makeTempDir() {
151+
return fs.mkdtempSync(path.join(os.tmpdir(), path.basename(__filename)));
152+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import * as fs from 'fs-extra';
2+
import * as path from 'path';
3+
import * as zlib from 'zlib';
4+
5+
import {
6+
Assembly,
7+
SPEC_FILE_NAME,
8+
SPEC_FILE_NAME_COMPRESSED,
9+
} from './assembly';
10+
import { validateAssembly } from './validate-assembly';
11+
12+
/**
13+
* Finds the path to the SPEC_FILE_NAME file, which will either
14+
* be the assembly or hold instructions to find the assembly.
15+
*
16+
* @param directory path to a directory with an assembly file
17+
* @returns path to the SPEC_FILE_NAME file
18+
*/
19+
export function getAssemblyFile(directory: string) {
20+
const dotJsiiFile = path.join(directory, SPEC_FILE_NAME);
21+
22+
if (!fs.existsSync(dotJsiiFile)) {
23+
throw new Error(
24+
`Expected to find ${SPEC_FILE_NAME} file in ${directory}, but no such file found`,
25+
);
26+
}
27+
28+
return dotJsiiFile;
29+
}
30+
31+
/**
32+
* Writes the assembly file either as .jsii or .jsii.gz if zipped
33+
*
34+
* @param directory the directory path to place the assembly file
35+
* @param assembly the contents of the assembly
36+
* @param compress whether or not to zip the assembly (.jsii.gz)
37+
* @returns whether or not the assembly was zipped
38+
*/
39+
export function writeAssembly(
40+
directory: string,
41+
assembly: Assembly,
42+
{ compress = false }: { compress?: boolean } = {},
43+
) {
44+
if (compress) {
45+
// write .jsii file with instructions on opening the compressed file
46+
fs.writeJsonSync(path.join(directory, SPEC_FILE_NAME), {
47+
schema: 'jsii/file-redirect',
48+
compression: 'gzip',
49+
filename: SPEC_FILE_NAME_COMPRESSED,
50+
});
51+
52+
// write actual assembly contents in .jsii.gz
53+
fs.writeFileSync(
54+
path.join(directory, SPEC_FILE_NAME_COMPRESSED),
55+
zlib.gzipSync(JSON.stringify(assembly)),
56+
);
57+
} else {
58+
fs.writeJsonSync(path.join(directory, SPEC_FILE_NAME), assembly, {
59+
encoding: 'utf8',
60+
spaces: 2,
61+
});
62+
}
63+
64+
return compress;
65+
}
66+
67+
/**
68+
* Loads the assembly file and, if present, follows instructions
69+
* found in the file to unzip compressed assemblies.
70+
*
71+
* @param directory the directory of the assembly file
72+
* @param validate whether to validate the contents of the file
73+
* @returns the assembly file as json
74+
*/
75+
export function loadAssemblyFromPath(
76+
directory: string,
77+
validate = true,
78+
): Assembly {
79+
const assemblyFile = getAssemblyFile(directory);
80+
return loadAssemblyFromFile(assemblyFile, validate);
81+
}
82+
83+
/**
84+
* Loads the assembly file and, if present, follows instructions
85+
* found in the file to unzip compressed assemblies.
86+
*
87+
* @param pathToFile the path to the SPEC_FILE_NAME file
88+
* @param validate whether to validate the contents of the file
89+
* @returns the assembly file as json
90+
*/
91+
export function loadAssemblyFromFile(
92+
pathToFile: string,
93+
validate = true,
94+
): Assembly {
95+
let contents = readAssembly(pathToFile);
96+
97+
// check if the file holds instructions to the actual assembly file
98+
if (contents.schema === 'jsii/file-redirect') {
99+
contents = findRedirectAssembly(pathToFile, contents);
100+
}
101+
102+
return validate ? validateAssembly(contents) : (contents as Assembly);
103+
}
104+
105+
function readAssembly(pathToFile: string) {
106+
return fs.readJsonSync(pathToFile, {
107+
encoding: 'utf-8',
108+
});
109+
}
110+
111+
function findRedirectAssembly(
112+
pathToFile: string,
113+
contents: Record<string, string>,
114+
) {
115+
validateRedirectSchema(contents);
116+
const redirectAssemblyFile = path.join(
117+
path.dirname(pathToFile),
118+
contents.filename,
119+
);
120+
return JSON.parse(
121+
zlib.gunzipSync(fs.readFileSync(redirectAssemblyFile)).toString(),
122+
);
123+
}
124+
125+
function validateRedirectSchema(contents: Record<string, string>) {
126+
const errors = [];
127+
if (contents.compression !== 'gzip') {
128+
errors.push(
129+
`compression must be 'gzip' but received '${contents.compression}'`,
130+
);
131+
}
132+
if (contents.filename === undefined) {
133+
errors.push("schema must include property 'filename'");
134+
}
135+
136+
if (errors.length !== 0) {
137+
throw new Error(`Invalid redirect schema:\n ${errors.join('\n ')}`);
138+
}
139+
}

packages/@jsii/spec/src/assembly.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
/**
2+
* Expected file name for jsii assembly or instructions to compressed assembly.
3+
*/
14
export const SPEC_FILE_NAME = '.jsii';
25

6+
/**
7+
* Expected file name for compressed assemblies.
8+
*/
9+
export const SPEC_FILE_NAME_COMPRESSED = `${SPEC_FILE_NAME}.gz`;
10+
311
/**
412
* A JSII assembly specification.
513
*/

packages/@jsii/spec/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './assembly';
2+
export * from './assembly-utils';
23
export * from './configuration';
34
export * from './name-tree';
45
export * from './validate-assembly';

packages/jsii/lib/assembler.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as spec from '@jsii/spec';
2-
import { PackageJson } from '@jsii/spec';
2+
import { writeAssembly, SPEC_FILE_NAME, PackageJson } from '@jsii/spec';
33
import * as chalk from 'chalk';
44
import * as crypto from 'crypto';
55
import * as deepEqual from 'fast-deep-equal/es6';
@@ -276,11 +276,13 @@ export class Assembler implements Emitter {
276276
const validator = new Validator(this.projectInfo, assembly);
277277
const validationResult = validator.emit();
278278
if (!validationResult.emitSkipped) {
279-
const assemblyPath = path.join(this.projectInfo.projectRoot, '.jsii');
280-
LOG.trace(`Emitting assembly: ${chalk.blue(assemblyPath)}`);
281-
fs.writeJsonSync(assemblyPath, _fingerprint(assembly), {
282-
encoding: 'utf8',
283-
spaces: 2,
279+
LOG.trace(
280+
`Emitting assembly: ${chalk.blue(
281+
path.join(this.projectInfo.projectRoot, SPEC_FILE_NAME),
282+
)}`,
283+
);
284+
writeAssembly(this.projectInfo.projectRoot, _fingerprint(assembly), {
285+
compress: false,
284286
});
285287
}
286288

0 commit comments

Comments
 (0)