Skip to content

Commit f4d9c7f

Browse files
kmontagjoshuafcole
andauthored
feat: Add typescript declaration file gen support via pbts (#5)
* Add typescript declaration file gen support via pbts * Fix specs in CI, add test case * Add nodejs github workflow * Remove unnecessary github workflow * Add package-lock.json * Add webpack's memfs to compile return value * Some style and typing fixes in main file * Tests for basic pbts functionality * Fix linting issues * Disallow pbts + json at the schema level, fixes in main file * Spec fixes and schema improvements * Fix lint issues * No need to export filesystem, actually * Process additional pbts arguments * Add specs for additional pbts use cases * Style fix * README tweak Co-authored-by: Joshua Cole <[email protected]> Co-authored-by: Kevin Montag <[email protected]>
1 parent 6a7506a commit f4d9c7f

File tree

6 files changed

+406
-24
lines changed

6 files changed

+406
-24
lines changed

README.md

+24-7
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,40 @@ module.exports = {
3131
use: {
3232
loader: 'protobufjs-loader',
3333
options: {
34-
/* controls the "target" flag to pbjs - true for
34+
/* Controls the "target" flag to pbjs - true for
3535
* json-module, false for static-module.
36+
*
3637
* default: false
3738
*/
38-
json: true,
39+
json: false,
3940

40-
/* import paths provided to pbjs.
41+
/* Import paths provided to pbjs.
42+
*
4143
* default: webpack import paths (i.e. config.resolve.modules)
4244
*/
4345
paths: ['/path/to/definitions'],
4446

45-
/* additional command line arguments passed to
46-
* pbjs, see https://github.com/dcodeIO/ProtoBuf.js/#pbjs-for-javascript
47-
* for a list of what's available.
47+
/* Additional command line arguments passed to pbjs.
48+
*
4849
* default: []
4950
*/
50-
pbjsArgs: ['--no-encode']
51+
pbjsArgs: ['--no-encode'],
52+
53+
/* Enable Typescript declaration file generation via pbts.
54+
*
55+
* Declaration files will be written every time the loader runs.
56+
* They'll be saved in the same directory as the protobuf file
57+
* being processed, with a `.d.ts` extension.
58+
*
59+
* This can be a config object or a boolean.
60+
*
61+
* default: false
62+
*/
63+
pbts: {
64+
/* Additional command line arguments passed to pbts.
65+
*/
66+
args: ['--no-comments'],
67+
}
5168
}
5269
}
5370
}]

index.js

+103-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const fs = require('fs');
2-
const { pbjs } = require('protobufjs/cli');
2+
const { pbjs, pbts } = require('protobufjs/cli');
33
const protobuf = require('protobufjs');
44
const tmp = require('tmp-promise');
55
const validateOptions = require('schema-utils').validate;
@@ -12,17 +12,67 @@ const schema = {
1212
properties: {
1313
json: {
1414
type: 'boolean',
15+
default: false,
1516
},
1617
paths: {
1718
type: 'array',
1819
},
1920
pbjsArgs: {
2021
type: 'array',
22+
default: [],
23+
},
24+
pbts: {
25+
oneOf: [
26+
{
27+
type: 'boolean',
28+
},
29+
{
30+
type: 'object',
31+
properties: {
32+
args: {
33+
type: 'array',
34+
default: [],
35+
},
36+
},
37+
additionalProperties: false,
38+
},
39+
],
40+
default: false,
2141
},
2242
},
43+
44+
// pbts config is only applicable if the pbjs target is
45+
// `static-module`, i.e. if the `json` flag is false. We enforce
46+
// this at the schema level; see
47+
// https://json-schema.org/understanding-json-schema/reference/conditionals.html#implication.
48+
anyOf: [
49+
{
50+
properties: {
51+
json: { const: true },
52+
pbts: { const: false },
53+
},
54+
},
55+
{
56+
not: {
57+
properties: { json: { const: true } },
58+
},
59+
},
60+
],
2361
additionalProperties: false,
2462
};
2563

64+
/**
65+
* Shared type for the validated options object, with no missing
66+
* properties (i.e. the user-provided object merged with default
67+
* values).
68+
*
69+
* @typedef {{ args: string[] }} PbtsOptions
70+
* @typedef {{
71+
* json: boolean, paths: string[], pbjsArgs: string[],
72+
* pbts: boolean | PbtsOptions
73+
* }} LoaderOptions
74+
*/
75+
2676
/**
2777
* We're supporting multiple webpack versions, so there are several
2878
* different possible structures for the `this` context in our loader
@@ -31,12 +81,47 @@ const schema = {
3181
* The `never` generic in the v5 context sets the return type of
3282
* `getOptions`. Since we're using the deprecated `loader-utils`
3383
* method of fetching options, this should be fine; however, if we
34-
* drop support for older webpack versions, we'll want to define a
35-
* stricter type for the options object.
84+
* drop support for older webpack versions, we'll want to switch to
85+
* using `getOptions`.
3686
*
3787
* @typedef { import('webpack').LoaderContext<never> | import('webpack4').loader.LoaderContext | import('webpack3').loader.LoaderContext | import('webpack2').loader.LoaderContext } LoaderContext
3888
*/
3989

90+
/** @type { (resourcePath: string, pbtsOptions: true | PbtsOptions, compiledContent: string, callback: NonNullable<ReturnType<LoaderContext['async']>>) => any } */
91+
const execPbts = (resourcePath, pbtsOptions, compiledContent, callback) => {
92+
/** @type PbtsOptions */
93+
const normalizedOptions = {
94+
args: [],
95+
...(pbtsOptions === true ? {} : pbtsOptions),
96+
};
97+
98+
// pbts CLI only supports streaming from stdin without a lot of
99+
// duplicated logic, so we need to use a tmp file. :(
100+
tmp
101+
.file({ postfix: '.js' })
102+
.then(
103+
(o) =>
104+
new Promise((resolve, reject) => {
105+
fs.write(o.fd, compiledContent, (err) => {
106+
if (err) {
107+
reject(err);
108+
} else {
109+
resolve(o.path);
110+
}
111+
});
112+
})
113+
)
114+
.then((compiledFilename) => {
115+
const declarationFilename = `${resourcePath}.d.ts`;
116+
const pbtsArgs = ['-o', declarationFilename]
117+
.concat(normalizedOptions.args)
118+
.concat([compiledFilename]);
119+
pbts.main(pbtsArgs, (err) => {
120+
callback(err, compiledContent);
121+
});
122+
});
123+
};
124+
40125
/** @type { (this: LoaderContext, source: string) => any } */
41126
module.exports = function protobufJsLoader(source) {
42127
const callback = this.async();
@@ -64,17 +149,25 @@ module.exports = function protobufJsLoader(source) {
64149
return undefined;
65150
})();
66151

67-
/** @type {{ json: boolean, paths: string[], pbjsArgs: string[] }} */
152+
/** @type LoaderOptions */
68153
const options = {
69154
json: false,
70155

71156
// Default to the paths given to the compiler.
72157
paths: defaultPaths || [],
73158

74159
pbjsArgs: [],
160+
161+
pbts: false,
162+
75163
...getOptions(this),
76164
};
77-
validateOptions(schema, options, { name: 'protobufjs-loader' });
165+
try {
166+
validateOptions(schema, options, { name: 'protobufjs-loader' });
167+
} catch (err) {
168+
callback(err instanceof Error ? err : new Error(`${err}`), undefined);
169+
return;
170+
}
78171

79172
/** @type { string } */
80173
let filename;
@@ -161,7 +254,11 @@ module.exports = function protobufJsLoader(source) {
161254
callback(depErr);
162255
})
163256
.then(() => {
164-
callback(err, result);
257+
if (!options.pbts || err) {
258+
callback(err, result);
259+
} else {
260+
execPbts(self.resourcePath, options.pbts, result || '', callback);
261+
}
165262
});
166263
});
167264
});

package-lock.json

+46
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
},
3030
"devDependencies": {
3131
"@types/chai": "^4.3.1",
32+
"@types/glob": "^7.2.0",
3233
"@types/loader-utils": "^2.0.3",
3334
"@types/memory-fs": "^0.3.3",
3435
"@types/mocha": "^8.2.3",
36+
"@types/tmp": "^0.2.3",
3537
"@types/webpack2": "npm:@types/webpack@^2.0.0",
3638
"@types/webpack3": "npm:@types/webpack@^3.0.0",
3739
"@types/webpack4": "npm:@types/webpack@^4.0.0",

test/helpers/compile.js

+18-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const isWebpack5 =
3434
* @typedef {{ arguments: [string], context: import('../../index').LoaderContext, options: never }} InspectLoaderResult
3535
*/
3636

37-
/** @type { (fixture: string, loaderOpts?: object, webpackOpts?: object) => Promise<InspectLoaderResult> } */
37+
/** @type { (fixture: string, loaderOpts?: object, webpackOpts?: object) => Promise<{ inspect: InspectLoaderResult }> } */
3838
module.exports = function compile(fixture, loaderOpts, webpackOpts) {
3939
return new Promise((resolve, reject) => {
4040
/** @type { InspectLoaderResult } */
@@ -108,6 +108,20 @@ module.exports = function compile(fixture, loaderOpts, webpackOpts) {
108108
}
109109
if (stats) {
110110
if (stats.hasErrors()) {
111+
if ('compilation' in stats) {
112+
/** @type Error */
113+
// The `stats` object appears to be incorrectly typed;
114+
// this compilation field exists in practice.
115+
//
116+
// @ts-ignore
117+
const compilationErr = stats.compilation.errors[0];
118+
if (compilationErr) {
119+
return compilationErr;
120+
}
121+
}
122+
123+
// fallback in case no specific error was found above for
124+
// some reason.
111125
return 'compilation error';
112126
}
113127
if (stats.hasWarnings()) {
@@ -120,7 +134,9 @@ module.exports = function compile(fixture, loaderOpts, webpackOpts) {
120134
if (problem) {
121135
reject(problem);
122136
} else {
123-
resolve(inspect);
137+
resolve({
138+
inspect,
139+
});
124140
}
125141
});
126142
});

0 commit comments

Comments
 (0)