Skip to content

Commit cc3e9a8

Browse files
authored
feat: allow configuring jsii namespaces for files-as-submodules (in addition to directories) (#2407)
When a submodule is created, it needs to have a namespace assigned for each target language (e.g. `My.Library.SubmoduleName`, `my_library.submodule_name`, etc).. Those names are usually derived, but can be configured explicitly via a `.jsiirc.json` file. We already used to look for `.jsiirc.json` in the same directory to read the jsii language namespaces for the classes in the subdirectories, but this was intended for use only when a *directory* is exported as a submodule. It starts to conflict when multiple source files in the same directory are exported as submodules: they would all share the same `.jsiirc.json` file, but that would mean they all share the same submodule namespaces which is nonsenical. Consider: ``` - root.ts export * as submodule from './submodule'; - submodule +-- index.ts export * as subsubmodule from './subsubmodule'; +-- subsubmodule.ts +-- .jsiirc.json // <-- gets used for both submodules! ``` In this PR, we make it so the directory-level `.jsiirc.json` is only read if the exported file is `index.ts`. This works both when the directory is exported, as well as if the file is directly exported. Otherwise, for files-as-submodules, read the jsii config from `.<file-base-name>.jsiirc.json`. This also adds a validation for multiple submodules not accidentally sharing the same Python module name (or Go, or Java, ...), which did in fact happen for us but doesn't make sense and should be caught. --- 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 6d2440c commit cc3e9a8

File tree

3 files changed

+91
-3
lines changed

3 files changed

+91
-3
lines changed

src/assembler.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { normalizeConfigPath } from './helpers';
1717
import { JsiiDiagnostic } from './jsii-diagnostic';
1818
import * as literate from './literate';
1919
import * as bindings from './node-bindings';
20-
import { ProjectInfo } from './project-info';
20+
import { AssemblyTargets, ProjectInfo } from './project-info';
2121
import { isReservedName } from './reserved-words';
2222
import { Sets } from './sets';
2323
import { DeprecatedRemover } from './transforms/deprecated-remover';
@@ -181,6 +181,8 @@ export class Assembler implements Emitter {
181181

182182
this.validateTypesAgainstPositions();
183183

184+
this.validateSubmoduleConfigs();
185+
184186
// Skip emitting if any diagnostic message is an error
185187
if (this._diagnostics.find((diag) => diag.category === ts.DiagnosticCategory.Error) != null) {
186188
LOG.debug('Skipping emit due to errors.');
@@ -705,8 +707,17 @@ export class Assembler implements Emitter {
705707
}
706708

707709
function loadSubmoduleTargetConfig(submoduleMain: string): SubmoduleSpec['targets'] {
708-
const jsiirc = path.resolve(submoduleMain, '..', '.jsiirc.json');
709-
if (!fs.existsSync(jsiirc)) {
710+
const dirname = path.dirname(submoduleMain);
711+
const basenameWithoutExtension = path.basename(submoduleMain).replace(/\.ts$/, '');
712+
713+
let jsiirc;
714+
if (basenameWithoutExtension === 'index') {
715+
jsiirc = path.resolve(submoduleMain, '..', '.jsiirc.json');
716+
} else {
717+
jsiirc = path.resolve(dirname, `.${basenameWithoutExtension}.jsiirc.json`);
718+
}
719+
720+
if (!jsiirc || !fs.existsSync(jsiirc)) {
710721
return undefined;
711722
}
712723
const data = JSON.parse(fs.readFileSync(jsiirc, 'utf-8'));
@@ -2749,6 +2760,56 @@ export class Assembler implements Emitter {
27492760
});
27502761
}
27512762
}
2763+
2764+
/**
2765+
* Make sure that no 2 submodules are emitting into the same target namespaces
2766+
*/
2767+
private validateSubmoduleConfigs() {
2768+
const self = this;
2769+
const dotNetnamespaces: Record<string, string[]> = {};
2770+
const javaPackages: Record<string, string[]> = {};
2771+
const pythonModules: Record<string, string[]> = {};
2772+
const goPackages: Record<string, string[]> = {};
2773+
2774+
for (const submodule of this._submodules.values()) {
2775+
const targets = submodule.targets as AssemblyTargets | undefined;
2776+
2777+
if (targets?.dotnet?.namespace) {
2778+
accumList(dotNetnamespaces, targets.dotnet.namespace, submodule.fqn);
2779+
}
2780+
if (targets?.java?.package) {
2781+
accumList(javaPackages, targets.java.package, submodule.fqn);
2782+
}
2783+
if (targets?.python?.module) {
2784+
accumList(pythonModules, targets.python.module, submodule.fqn);
2785+
}
2786+
if (targets?.go?.packageName) {
2787+
accumList(goPackages, targets.go.packageName, submodule.fqn);
2788+
}
2789+
}
2790+
2791+
maybeError('dotnet', dotNetnamespaces);
2792+
maybeError('java', javaPackages);
2793+
maybeError('python', pythonModules);
2794+
maybeError('go', goPackages);
2795+
2796+
function accumList(set: Record<string, string[]>, key: string, value: string) {
2797+
if (!set[key]) {
2798+
set[key] = [];
2799+
}
2800+
set[key].push(value);
2801+
}
2802+
2803+
function maybeError(language: string, set: Record<string, string[]>) {
2804+
for (const [namespace, modules] of Object.entries(set)) {
2805+
if (modules.length > 1) {
2806+
self._diagnostics.push(
2807+
JsiiDiagnostic.JSII_4010_SUBMODULE_NAMESPACE_CONFLICT.create(undefined, language, namespace, modules),
2808+
);
2809+
}
2810+
}
2811+
}
2812+
}
27522813
}
27532814

27542815
export interface AssemblerOptions {

src/jsii-diagnostic.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,13 @@ export class JsiiDiagnostic implements ts.Diagnostic {
479479
name: 'typescript-config/disabled-tsconfig-validation',
480480
});
481481

482+
public static readonly JSII_4010_SUBMODULE_NAMESPACE_CONFLICT = Code.warning({
483+
code: 4010,
484+
formatter: (language: string, namespace: string, modules: string[]) =>
485+
`Multiple modules emit to the same ${language} namespace "${namespace}": ${modules.join(', ')}`,
486+
name: 'jsii-config/submodule-conflict',
487+
});
488+
482489
//////////////////////////////////////////////////////////////////////////////
483490
// 5000 => 5999 -- LANGUAGE COMPATIBILITY ERRORS
484491

test/submodules.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,26 @@ test('submodules loaded from directories can have targets', () => {
5555
);
5656
});
5757

58+
test('submodules loaded from files can have targets', () => {
59+
const assembly = sourceToAssemblyHelper({
60+
'index.ts': 'export * as submodule from "./subfile"',
61+
'subfile.ts': 'export class Foo { }',
62+
'.subfile.jsiirc.json': JSON.stringify({
63+
targets: {
64+
python: 'fun',
65+
},
66+
}),
67+
});
68+
69+
expect(assembly.submodules!['testpkg.submodule']).toEqual(
70+
expect.objectContaining({
71+
targets: {
72+
python: 'fun',
73+
},
74+
}),
75+
);
76+
});
77+
5878
test('submodule READMEs can have literate source references', () => {
5979
const assembly = sourceToAssemblyHelper({
6080
'index.ts': 'export * as submodule from "./subdir"',

0 commit comments

Comments
 (0)