diff --git a/src/assembler.ts b/src/assembler.ts index c268f3dd..7bc9039b 100644 --- a/src/assembler.ts +++ b/src/assembler.ts @@ -17,7 +17,7 @@ import { normalizeConfigPath } from './helpers'; import { JsiiDiagnostic } from './jsii-diagnostic'; import * as literate from './literate'; import * as bindings from './node-bindings'; -import { ProjectInfo } from './project-info'; +import { AssemblyTargets, ProjectInfo } from './project-info'; import { isReservedName } from './reserved-words'; import { Sets } from './sets'; import { DeprecatedRemover } from './transforms/deprecated-remover'; @@ -181,6 +181,8 @@ export class Assembler implements Emitter { this.validateTypesAgainstPositions(); + this.validateSubmoduleConfigs(); + // Skip emitting if any diagnostic message is an error if (this._diagnostics.find((diag) => diag.category === ts.DiagnosticCategory.Error) != null) { LOG.debug('Skipping emit due to errors.'); @@ -705,8 +707,17 @@ export class Assembler implements Emitter { } function loadSubmoduleTargetConfig(submoduleMain: string): SubmoduleSpec['targets'] { - const jsiirc = path.resolve(submoduleMain, '..', '.jsiirc.json'); - if (!fs.existsSync(jsiirc)) { + const dirname = path.dirname(submoduleMain); + const basenameWithoutExtension = path.basename(submoduleMain).replace(/\.ts$/, ''); + + let jsiirc; + if (basenameWithoutExtension === 'index') { + jsiirc = path.resolve(submoduleMain, '..', '.jsiirc.json'); + } else { + jsiirc = path.resolve(dirname, `.${basenameWithoutExtension}.jsiirc.json`); + } + + if (!jsiirc || !fs.existsSync(jsiirc)) { return undefined; } const data = JSON.parse(fs.readFileSync(jsiirc, 'utf-8')); @@ -2749,6 +2760,56 @@ export class Assembler implements Emitter { }); } } + + /** + * Make sure that no 2 submodules are emitting into the same target namespaces + */ + private validateSubmoduleConfigs() { + const self = this; + const dotNetnamespaces: Record = {}; + const javaPackages: Record = {}; + const pythonModules: Record = {}; + const goPackages: Record = {}; + + for (const submodule of this._submodules.values()) { + const targets = submodule.targets as AssemblyTargets | undefined; + + if (targets?.dotnet?.namespace) { + accumList(dotNetnamespaces, targets.dotnet.namespace, submodule.fqn); + } + if (targets?.java?.package) { + accumList(javaPackages, targets.java.package, submodule.fqn); + } + if (targets?.python?.module) { + accumList(pythonModules, targets.python.module, submodule.fqn); + } + if (targets?.go?.packageName) { + accumList(goPackages, targets.go.packageName, submodule.fqn); + } + } + + maybeError('dotnet', dotNetnamespaces); + maybeError('java', javaPackages); + maybeError('python', pythonModules); + maybeError('go', goPackages); + + function accumList(set: Record, key: string, value: string) { + if (!set[key]) { + set[key] = []; + } + set[key].push(value); + } + + function maybeError(language: string, set: Record) { + for (const [namespace, modules] of Object.entries(set)) { + if (modules.length > 1) { + self._diagnostics.push( + JsiiDiagnostic.JSII_4010_SUBMODULE_NAMESPACE_CONFLICT.create(undefined, language, namespace, modules), + ); + } + } + } + } } export interface AssemblerOptions { diff --git a/src/jsii-diagnostic.ts b/src/jsii-diagnostic.ts index 5f02b195..568c9985 100644 --- a/src/jsii-diagnostic.ts +++ b/src/jsii-diagnostic.ts @@ -479,6 +479,13 @@ export class JsiiDiagnostic implements ts.Diagnostic { name: 'typescript-config/disabled-tsconfig-validation', }); + public static readonly JSII_4010_SUBMODULE_NAMESPACE_CONFLICT = Code.warning({ + code: 4010, + formatter: (language: string, namespace: string, modules: string[]) => + `Multiple modules emit to the same ${language} namespace "${namespace}": ${modules.join(', ')}`, + name: 'jsii-config/submodule-conflict', + }); + ////////////////////////////////////////////////////////////////////////////// // 5000 => 5999 -- LANGUAGE COMPATIBILITY ERRORS diff --git a/test/submodules.test.ts b/test/submodules.test.ts index 0fd6f7a5..da6d80d2 100644 --- a/test/submodules.test.ts +++ b/test/submodules.test.ts @@ -55,6 +55,26 @@ test('submodules loaded from directories can have targets', () => { ); }); +test('submodules loaded from files can have targets', () => { + const assembly = sourceToAssemblyHelper({ + 'index.ts': 'export * as submodule from "./subfile"', + 'subfile.ts': 'export class Foo { }', + '.subfile.jsiirc.json': JSON.stringify({ + targets: { + python: 'fun', + }, + }), + }); + + expect(assembly.submodules!['testpkg.submodule']).toEqual( + expect.objectContaining({ + targets: { + python: 'fun', + }, + }), + ); +}); + test('submodule READMEs can have literate source references', () => { const assembly = sourceToAssemblyHelper({ 'index.ts': 'export * as submodule from "./subdir"',