UNPKG

9.15 kBJavaScriptView Raw
1"use strict";
2/**
3 * Helper routines for use with the jsii compiler
4 *
5 * These are mostly used for testing, but all projects that need to exercise
6 * the JSII compiler to test something need to share this code, so might as
7 * well put it in one reusable place.
8 */
9Object.defineProperty(exports, "__esModule", { value: true });
10exports.TestWorkspace = exports.compileJsiiForTest = exports.sourceToAssemblyHelper = void 0;
11const fs = require("node:fs");
12const os = require("node:os");
13const path = require("node:path");
14const spec_1 = require("@jsii/spec");
15const typescript_1 = require("typescript");
16const compiler_1 = require("./compiler");
17const project_info_1 = require("./project-info");
18const utils_1 = require("./utils");
19/**
20 * Compile a piece of source and return the JSII assembly for it
21 *
22 * Only usable for trivial cases and tests.
23 *
24 * @param source can either be a single `string` (the content of `index.ts`), or
25 * a map of fileName to content, which *must* include `index.ts`.
26 * @param options accepts a callback for historical reasons but really expects to
27 * take an options object.
28 */
29function sourceToAssemblyHelper(source, options) {
30 return compileJsiiForTest(source, options).assembly;
31}
32exports.sourceToAssemblyHelper = sourceToAssemblyHelper;
33/**
34 * Compile a piece of source and return the assembly and compiled sources for it
35 *
36 * Only usable for trivial cases and tests.
37 *
38 * @param source can either be a single `string` (the content of `index.ts`), or
39 * a map of fileName to content, which *must* include `index.ts`.
40 * @param options accepts a callback for historical reasons but really expects to
41 * take an options object.
42 */
43function compileJsiiForTest(source, options, compilerOptions) {
44 if (typeof source === 'string') {
45 source = { 'index.ts': source };
46 }
47 const inSomeLocation = isOptionsObject(options) && options.compilationDirectory ? inOtherDir(options.compilationDirectory) : inTempDir;
48 // Easiest way to get the source into the compiler is to write it to disk somewhere.
49 // I guess we could make an in-memory compiler host but that seems like work...
50 return inSomeLocation(() => {
51 for (const [fileName, content] of Object.entries(source)) {
52 fs.mkdirSync(path.dirname(fileName), { recursive: true });
53 fs.writeFileSync(fileName, content, { encoding: 'utf-8' });
54 }
55 const { projectInfo, packageJson } = makeProjectInfo('index.ts', typeof options === 'function'
56 ? options
57 : (pi) => {
58 Object.assign(pi, options?.packageJson ?? options?.projectInfo ?? {});
59 });
60 const compiler = new compiler_1.Compiler({
61 projectInfo,
62 ...compilerOptions,
63 });
64 const emitResult = compiler.emit();
65 const errors = emitResult.diagnostics.filter((d) => d.category === typescript_1.DiagnosticCategory.Error);
66 for (const error of errors) {
67 console.error((0, utils_1.formatDiagnostic)(error, projectInfo.projectRoot));
68 // logDiagnostic() doesn't work out of the box, so console.error() it is.
69 }
70 if (errors.length > 0 || emitResult.emitSkipped) {
71 throw new Error('There were compiler errors');
72 }
73 const assembly = (0, spec_1.loadAssemblyFromPath)(process.cwd(), false);
74 const files = {};
75 for (const filename of Object.keys(source)) {
76 let jsFile = filename.replace(/\.ts$/, '.js');
77 let dtsFile = filename.replace(/\.ts$/, '.d.ts');
78 if (projectInfo.tsc?.outDir && filename !== 'README.md') {
79 jsFile = path.join(projectInfo.tsc.outDir, jsFile);
80 dtsFile = path.join(projectInfo.tsc.outDir, dtsFile);
81 }
82 // eslint-disable-next-line no-await-in-loop
83 files[jsFile] = fs.readFileSync(jsFile, { encoding: 'utf-8' });
84 // eslint-disable-next-line no-await-in-loop
85 files[dtsFile] = fs.readFileSync(dtsFile, { encoding: 'utf-8' });
86 const warningsFileName = '.warnings.jsii.js';
87 if (fs.existsSync(warningsFileName)) {
88 // eslint-disable-next-line no-await-in-loop
89 files[warningsFileName] = fs.readFileSync(warningsFileName, {
90 encoding: 'utf-8',
91 });
92 }
93 }
94 return {
95 assembly,
96 files,
97 packageJson,
98 compressAssembly: isOptionsObject(options) && options.compressAssembly ? true : false,
99 };
100 });
101}
102exports.compileJsiiForTest = compileJsiiForTest;
103function inTempDir(block) {
104 const origDir = process.cwd();
105 const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsii'));
106 process.chdir(tmpDir);
107 const ret = block();
108 process.chdir(origDir);
109 fs.rmSync(tmpDir, { force: true, recursive: true });
110 return ret;
111}
112function inOtherDir(dir) {
113 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
114 return (block) => {
115 const origDir = process.cwd();
116 process.chdir(dir);
117 try {
118 return block();
119 }
120 finally {
121 process.chdir(origDir);
122 }
123 };
124}
125/**
126 * Obtain project info so we can call the compiler
127 *
128 * Creating this directly in-memory leads to slightly different behavior from calling
129 * jsii from the command-line, and I don't want to figure out right now.
130 *
131 * Most consistent behavior seems to be to write a package.json to disk and
132 * then calling the same functions as the CLI would.
133 */
134function makeProjectInfo(types, cb) {
135 const packageJson = {
136 types,
137 main: types.replace(/(?:\.d)?\.ts(x?)/, '.js$1'),
138 name: 'testpkg', // That's what package.json would tell if we look up...
139 version: '0.0.1',
140 license: 'Apache-2.0',
141 author: { name: 'John Doe' },
142 repository: { type: 'git', url: 'https://github.com/aws/jsii.git' },
143 jsii: {},
144 };
145 if (cb) {
146 cb(packageJson);
147 }
148 fs.writeFileSync('package.json', JSON.stringify(packageJson, (_, v) => v, 2), 'utf-8');
149 const { projectInfo } = (0, project_info_1.loadProjectInfo)(path.resolve(process.cwd(), '.'));
150 return { projectInfo, packageJson };
151}
152function isOptionsObject(x) {
153 return x ? typeof x === 'object' : false;
154}
155/**
156 * An NPM-ready workspace where we can install test-compile dependencies and compile new assemblies
157 */
158class TestWorkspace {
159 /**
160 * Create a new workspace.
161 *
162 * Creates a temporary directory, don't forget to call cleanUp
163 */
164 static create() {
165 const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsii-testworkspace'));
166 fs.mkdirSync(tmpDir, { recursive: true });
167 return new TestWorkspace(tmpDir);
168 }
169 /**
170 * Execute a block with a temporary workspace
171 */
172 static withWorkspace(block) {
173 const ws = TestWorkspace.create();
174 try {
175 return block(ws);
176 }
177 finally {
178 ws.cleanup();
179 }
180 }
181 constructor(rootDirectory) {
182 this.rootDirectory = rootDirectory;
183 this.installed = new Set();
184 }
185 /**
186 * Add a test-compiled jsii assembly as a dependency
187 */
188 addDependency(dependencyAssembly) {
189 if (this.installed.has(dependencyAssembly.assembly.name)) {
190 throw new Error(`A dependency with name '${dependencyAssembly.assembly.name}' was already installed. Give one a different name.`);
191 }
192 this.installed.add(dependencyAssembly.assembly.name);
193 // The following is silly, however: the helper has compiled the given source to
194 // an assembly, and output files, and then removed their traces from disk.
195 // We need those files back on disk, so write them back out again.
196 //
197 // We will drop them in 'node_modules/<name>' so they can be imported
198 // as if they were installed.
199 const modDir = path.join(this.rootDirectory, 'node_modules', dependencyAssembly.assembly.name);
200 fs.mkdirSync(modDir, { recursive: true });
201 (0, spec_1.writeAssembly)(modDir, dependencyAssembly.assembly, {
202 compress: dependencyAssembly.compressAssembly,
203 });
204 fs.writeFileSync(path.join(modDir, 'package.json'), JSON.stringify(dependencyAssembly.packageJson, null, 2), 'utf-8');
205 for (const [fileName, fileContents] of Object.entries(dependencyAssembly.files)) {
206 fs.mkdirSync(path.dirname(path.join(modDir, fileName)), {
207 recursive: true,
208 });
209 fs.writeFileSync(path.join(modDir, fileName), fileContents);
210 }
211 }
212 dependencyDir(name) {
213 if (!this.installed.has(name)) {
214 throw new Error(`No dependency with name '${name}' has been installed`);
215 }
216 return path.join(this.rootDirectory, 'node_modules', name);
217 }
218 cleanup() {
219 fs.rmSync(this.rootDirectory, { force: true, recursive: true });
220 }
221}
222exports.TestWorkspace = TestWorkspace;
223//# sourceMappingURL=helpers.js.map
\No newline at end of file