Skip to content

Commit 8b29c3a

Browse files
kmontagkatel0k
andauthored
feat: add option for custom typescript output locations (#46)
This adds the `output` option to the `pbts` config, which accepts a function to map resource paths (i.e. protobuf file paths) to their typescript declaration (`.d.ts`) paths. This is an alternate approach to #43. --------- Co-authored-by: katel0k <[email protected]>
1 parent 19fb602 commit 8b29c3a

File tree

3 files changed

+148
-31
lines changed

3 files changed

+148
-31
lines changed

README.md

+21-2
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ module.exports = {
4848
/* Enable Typescript declaration file generation via pbts.
4949
*
5050
* Declaration files will be written every time the loader runs.
51-
* They'll be saved in the same directory as the protobuf file
52-
* being processed, with a `.d.ts` extension.
51+
* By default, they'll be saved in the same directory as the
52+
* protobuf file being processed, using the same filename with a
53+
* `.d.ts` extension.
5354
*
5455
* This only works if you're using the 'static-module' target
5556
* for pbjs (i.e. the default target).
@@ -63,6 +64,24 @@ module.exports = {
6364
/* Additional command line arguments passed to pbts.
6465
*/
6566
args: ['--no-comments'],
67+
68+
/* Optional function which receives the path to a protobuf file,
69+
* and returns the output path (or a promise resolving to the
70+
* output path) for the associated Typescript declaration file.
71+
*
72+
* If this is null (i.e. by default), declaration files will be
73+
* saved to `${protobufFile}.d.ts`.
74+
*
75+
* The loader won't create any directories on the filesystem. If
76+
* writing to a nonstandard location, you should ensure that it
77+
* exists and is writable.
78+
*
79+
* default: null
80+
*/
81+
output: (protobufFile) =>
82+
`/custom/location/${require('path').basename(
83+
protobufFile
84+
)}.d.ts`,
6685
},
6786

6887
/* Set the "target" flag to pbjs.

index.js

+47-28
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ const schema = {
3333
type: 'array',
3434
default: [],
3535
},
36+
output: {
37+
anyOf: [{ type: 'null' }, { instanceof: 'Function' }],
38+
default: null,
39+
},
3640
},
3741
additionalProperties: false,
3842
},
@@ -49,7 +53,7 @@ const schema = {
4953
* properties (i.e. the user-provided object merged with default
5054
* values).
5155
*
52-
* @typedef {{ args: string[] }} PbtsOptions
56+
* @typedef {{ args: string[], output: ((resourcePath: string) => string | Promise<string>) | null }} PbtsOptions
5357
* @typedef {{
5458
* paths: string[], pbjsArgs: string[],
5559
* pbts: boolean | PbtsOptions,
@@ -67,24 +71,26 @@ const schema = {
6771

6872
/** @type { (resourcePath: string, pbtsOptions: true | PbtsOptions, compiledContent: string, callback: NonNullable<ReturnType<LoaderContext['async']>>) => any } */
6973
const execPbts = (resourcePath, pbtsOptions, compiledContent, callback) => {
70-
/** @type PbtsOptions */
71-
const normalizedOptions = {
72-
args: [],
73-
...(pbtsOptions === true ? {} : pbtsOptions),
74-
};
75-
76-
// pbts CLI only supports streaming from stdin without a lot of
77-
// duplicated logic, so we need to use a tmp file. :(
78-
new Promise((resolve, reject) => {
79-
tmp.file({ postfix: '.js' }, (err, compiledFilename) => {
80-
if (err) {
81-
reject(err);
82-
} else {
83-
resolve(compiledFilename);
84-
}
85-
});
86-
})
87-
.then(
74+
try {
75+
/** @type PbtsOptions */
76+
const normalizedOptions = {
77+
args: [],
78+
output: null,
79+
...(pbtsOptions === true ? {} : pbtsOptions),
80+
};
81+
82+
// pbts CLI only supports streaming from stdin without a lot of
83+
// duplicated logic, so we need to use a tmp file. :(
84+
/** @type Promise<string> */
85+
const compiledFilenamePromise = new Promise((resolve, reject) => {
86+
tmp.file({ postfix: '.js' }, (err, compiledFilename) => {
87+
if (err) {
88+
reject(err);
89+
} else {
90+
resolve(compiledFilename);
91+
}
92+
});
93+
}).then(
8894
(compiledFilename) =>
8995
new Promise((resolve, reject) => {
9096
fs.writeFile(compiledFilename, compiledContent, (err) => {
@@ -95,16 +101,29 @@ const execPbts = (resourcePath, pbtsOptions, compiledContent, callback) => {
95101
}
96102
});
97103
})
98-
)
99-
.then((compiledFilename) => {
100-
const declarationFilename = `${resourcePath}.d.ts`;
101-
const pbtsArgs = ['-o', declarationFilename]
102-
.concat(normalizedOptions.args)
103-
.concat([compiledFilename]);
104-
pbts.main(pbtsArgs, (err) => {
105-
callback(err, compiledContent);
104+
);
105+
/** @type { (resourcePath: string) => string | Promise<string> } */
106+
const output =
107+
normalizedOptions.output === null
108+
? (r) => `${r}.d.ts`
109+
: normalizedOptions.output;
110+
const declarationFilenamePromise = Promise.resolve(output(resourcePath));
111+
112+
Promise.all([compiledFilenamePromise, declarationFilenamePromise])
113+
.then(([compiledFilename, declarationFilename]) => {
114+
const pbtsArgs = ['-o', declarationFilename]
115+
.concat(normalizedOptions.args)
116+
.concat([compiledFilename]);
117+
pbts.main(pbtsArgs, (err) => {
118+
callback(err, compiledContent);
119+
});
120+
})
121+
.catch((err) => {
122+
callback(err, undefined);
106123
});
107-
});
124+
} catch (err) {
125+
callback(err instanceof Error ? err : new Error(`${err}`), undefined);
126+
}
108127
};
109128

110129
/** @type { (this: LoaderContext, source: string) => any } */

test/index.test.js

+80-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ describe('protobufjs-loader', function () {
168168
compile(path.join(this.tmpDir, 'basic'), { pbts: true }).then(() => {
169169
// By default, definitions should just be siblings of their
170170
// associated .proto file.
171-
glob(path.join(this.tmpDir, '*.d.ts'), (globErr, files) => {
171+
glob(path.join(this.tmpDir, '**', '*.d.ts'), (globErr, files) => {
172172
if (globErr) {
173173
throw globErr;
174174
}
@@ -261,6 +261,85 @@ describe('protobufjs-loader', function () {
261261
});
262262
});
263263

264+
describe('with custom declaration output locations', function () {
265+
/**
266+
* Helper function to assert that declarations for the basic
267+
* fixture can be saved to a custom location. Allows providing
268+
* either a plain string location, or a promise resolving to
269+
* the location.
270+
*
271+
* Return a promise resolving to true as a simple sanity check
272+
* that all assertions completed successfully.
273+
*
274+
* @type { (tmpDir: string, location: string | Promise<string>) => Promise<boolean> }
275+
*/
276+
const assertSavesDeclarationToCustomLocation = (tmpDir, location) => {
277+
let outputInvocationCount = 0;
278+
279+
/**
280+
* @type { (input: string) => string | Promise<string> }
281+
*/
282+
const output = (input) => {
283+
outputInvocationCount += 1;
284+
assert.equal(
285+
fs.realpathSync(input),
286+
fs.realpathSync(path.join(tmpDir, 'basic.proto'))
287+
);
288+
return location;
289+
};
290+
291+
return compile(path.join(tmpDir, 'basic'), {
292+
pbts: {
293+
output,
294+
},
295+
}).then(() => {
296+
assert.equal(outputInvocationCount, 1);
297+
298+
return Promise.resolve(location).then((locationStr) => {
299+
const content = fs.readFileSync(locationStr).toString();
300+
assert.include(content, 'class Bar implements IBar');
301+
return true;
302+
});
303+
});
304+
};
305+
306+
it('should save a declaration file to a synchronously-generated location', function (done) {
307+
tmp.dir((err, altTmpDir, cleanup) => {
308+
if (err) {
309+
throw err;
310+
}
311+
assertSavesDeclarationToCustomLocation(
312+
this.tmpDir,
313+
path.join(altTmpDir, 'alt.d.ts')
314+
).then((result) => {
315+
assert.isTrue(result);
316+
cleanup();
317+
done();
318+
});
319+
});
320+
});
321+
322+
it('should save a declaration file to an asynchronously-generated location', function (done) {
323+
tmp.dir((err, altTmpDir, cleanup) => {
324+
if (err) {
325+
throw err;
326+
}
327+
assertSavesDeclarationToCustomLocation(
328+
this.tmpDir,
329+
new Promise((resolve) => {
330+
setTimeout(() => {
331+
resolve(path.join(altTmpDir, 'alt.d.ts'));
332+
}, 5);
333+
})
334+
).then((result) => {
335+
assert.isTrue(result);
336+
cleanup();
337+
done();
338+
});
339+
});
340+
});
341+
});
342+
264343
describe('with imports', function () {
265344
it('should compile imported definitions', function (done) {
266345
compile(path.join(this.tmpDir, 'import'), {

0 commit comments

Comments
 (0)