@vitronai/themis 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/docs/api.md +1 -0
- package/package.json +1 -1
- package/src/generate.js +62 -32
package/README.md
CHANGED
|
@@ -94,6 +94,7 @@ Themis is built for modern Node.js and TypeScript projects:
|
|
|
94
94
|
## Adopt In Another Repo
|
|
95
95
|
|
|
96
96
|
Use the AI Quickstart above as the canonical install/generate/test flow. Generated files land under `__themis__/tests` by default.
|
|
97
|
+
TypeScript-generated suites use `import` syntax so downstream ESLint and ESM-style rules do not flag Themis output as legacy `require(...)` code.
|
|
97
98
|
|
|
98
99
|
If another repo wants its agents to reliably choose Themis, put the framework choice directly in that repo's agent instructions instead of assuming agents will infer it from package metadata alone.
|
|
99
100
|
|
package/docs/api.md
CHANGED
|
@@ -62,6 +62,7 @@ Default behavior:
|
|
|
62
62
|
- input directory: `src`
|
|
63
63
|
- output directory: `__themis__/tests`
|
|
64
64
|
- generated files mirror the scanned source tree with `*.generated.test.ts` for TS/TSX sources and `*.generated.test.js` for JS/JSX sources
|
|
65
|
+
- generated TypeScript suites emit `import` syntax so downstream lint and ESM rules do not reject Themis output for using `require(...)`
|
|
65
66
|
- generated tests import their shared contract runtime from `@vitronai/themis/contract-runtime` instead of writing framework helper files into the repo
|
|
66
67
|
- generated tests assert normalized runtime export contracts directly in generated source
|
|
67
68
|
- scenario adapters cover React components, React hooks, Next app components, Next route handlers, generic route handlers, and Node service functions when inputs can be inferred or hinted
|
package/package.json
CHANGED
package/src/generate.js
CHANGED
|
@@ -3232,31 +3232,27 @@ function renderGeneratedTest({ projectRoot, helperFile, outputFile, analysis })
|
|
|
3232
3232
|
const helperImport = helperFile;
|
|
3233
3233
|
const sourceImport = normalizeRelativeModule(path.relative(path.dirname(outputFile), analysis.file));
|
|
3234
3234
|
const sourceAbsolutePath = normalizePath(path.relative(path.dirname(outputFile), analysis.file));
|
|
3235
|
+
const useImportSyntax = path.extname(outputFile).toLowerCase() === '.ts';
|
|
3235
3236
|
const expectedExportContracts = buildExpectedExportContracts(analysis);
|
|
3236
3237
|
const providerImport = analysis.projectProviderFile
|
|
3237
3238
|
? normalizeRelativeModule(path.relative(path.dirname(outputFile), analysis.projectProviderFile))
|
|
3238
3239
|
: null;
|
|
3239
3240
|
const suiteName = `generated contract > ${relativeSourcePath}`;
|
|
3240
3241
|
const exactExportBlock = analysis.exactExports
|
|
3241
|
-
? `test('matches scanned export names', () => {\n const moduleExports = loadModuleExports();\n expect(listExportNames(moduleExports)).toEqual(SCANNED_EXPORTS);\n });`
|
|
3242
|
-
: `test('exposes runtime exports after scan', () => {\n const moduleExports = loadModuleExports();\n expect(listExportNames(moduleExports).length).toBeTruthy();\n });`;
|
|
3242
|
+
? `test('matches scanned export names', async () => {\n const moduleExports = await loadModuleExports();\n expect(listExportNames(moduleExports)).toEqual(SCANNED_EXPORTS);\n });`
|
|
3243
|
+
: `test('exposes runtime exports after scan', async () => {\n const moduleExports = await loadModuleExports();\n expect(listExportNames(moduleExports).length).toBeTruthy();\n });`;
|
|
3244
|
+
const runtimePrelude = useImportSyntax
|
|
3245
|
+
? renderGeneratedImportPrelude({ helperImport, providerImport })
|
|
3246
|
+
: renderGeneratedRequirePrelude({ helperImport });
|
|
3247
|
+
const providerLoadExpression = useImportSyntax ? 'PROJECT_PROVIDER_MODULE' : 'require(PROJECT_PROVIDER_IMPORT)';
|
|
3248
|
+
const loadModuleBody = useImportSyntax
|
|
3249
|
+
? ' assertSourceFreshness(SOURCE_FILE, SOURCE_HASH, SOURCE_PATH, REGENERATE_COMMAND);\n return import(SOURCE_IMPORT);'
|
|
3250
|
+
: ' assertSourceFreshness(SOURCE_FILE, SOURCE_HASH, SOURCE_PATH, REGENERATE_COMMAND);\n const resolved = require.resolve(SOURCE_IMPORT);\n delete require.cache[resolved];\n return require(SOURCE_IMPORT);';
|
|
3243
3251
|
|
|
3244
3252
|
return `${GENERATED_MARKER}
|
|
3245
3253
|
// Source: ${relativeSourcePath}
|
|
3246
3254
|
|
|
3247
|
-
|
|
3248
|
-
const {
|
|
3249
|
-
listExportNames,
|
|
3250
|
-
buildModuleContract,
|
|
3251
|
-
readExportValue,
|
|
3252
|
-
normalizeBehaviorValue,
|
|
3253
|
-
normalizeRouteResult,
|
|
3254
|
-
createRequestFromSpec,
|
|
3255
|
-
assertSourceFreshness,
|
|
3256
|
-
runComponentInteractionContract,
|
|
3257
|
-
runComponentBehaviorFlowContract,
|
|
3258
|
-
runHookInteractionContract
|
|
3259
|
-
} = require(${JSON.stringify(helperImport)});
|
|
3255
|
+
${runtimePrelude}
|
|
3260
3256
|
|
|
3261
3257
|
const SOURCE_PATH = ${JSON.stringify(relativeSourcePath)};
|
|
3262
3258
|
const SOURCE_IMPORT = ${JSON.stringify(sourceImport)};
|
|
@@ -3276,10 +3272,7 @@ const ROUTE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'r
|
|
|
3276
3272
|
const SERVICE_CASES = ${JSON.stringify(flattenScenarioCases(analysis.scenarios, 'node-service'), null, 2)};
|
|
3277
3273
|
|
|
3278
3274
|
function loadModuleExports() {
|
|
3279
|
-
|
|
3280
|
-
const resolved = require.resolve(SOURCE_IMPORT);
|
|
3281
|
-
delete require.cache[resolved];
|
|
3282
|
-
return require(SOURCE_IMPORT);
|
|
3275
|
+
${loadModuleBody}
|
|
3283
3276
|
}
|
|
3284
3277
|
|
|
3285
3278
|
function applyProjectProviderMocks(exportName, scenarioName) {
|
|
@@ -3287,7 +3280,7 @@ function applyProjectProviderMocks(exportName, scenarioName) {
|
|
|
3287
3280
|
return;
|
|
3288
3281
|
}
|
|
3289
3282
|
|
|
3290
|
-
const loaded =
|
|
3283
|
+
const loaded = ${providerLoadExpression};
|
|
3291
3284
|
const providers = Array.isArray(loaded)
|
|
3292
3285
|
? loaded
|
|
3293
3286
|
: Array.isArray(loaded && loaded.providers)
|
|
@@ -3328,7 +3321,7 @@ function applyProjectProviderRender(element, exportName, scenarioName) {
|
|
|
3328
3321
|
return current;
|
|
3329
3322
|
}
|
|
3330
3323
|
|
|
3331
|
-
const loaded =
|
|
3324
|
+
const loaded = ${providerLoadExpression};
|
|
3332
3325
|
const providers = Array.isArray(loaded)
|
|
3333
3326
|
? loaded
|
|
3334
3327
|
: Array.isArray(loaded && loaded.providers)
|
|
@@ -3679,8 +3672,8 @@ function assertServiceResultContractShape(result) {
|
|
|
3679
3672
|
describe(${JSON.stringify(suiteName)}, () => {
|
|
3680
3673
|
${exactExportBlock}
|
|
3681
3674
|
|
|
3682
|
-
test('captures runtime export contract', () => {
|
|
3683
|
-
const moduleExports = loadModuleExports();
|
|
3675
|
+
test('captures runtime export contract', async () => {
|
|
3676
|
+
const moduleExports = await loadModuleExports();
|
|
3684
3677
|
const runtime = buildModuleContract(moduleExports);
|
|
3685
3678
|
assertExpectedRuntimeContract(runtime);
|
|
3686
3679
|
});
|
|
@@ -3688,9 +3681,9 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3688
3681
|
if (NEXT_APP_CASES.length > 0) {
|
|
3689
3682
|
describe('next app component adapter', () => {
|
|
3690
3683
|
for (const testCase of NEXT_APP_CASES) {
|
|
3691
|
-
test(testCase.exportName + ' ' + testCase.caseName, () => {
|
|
3684
|
+
test(testCase.exportName + ' ' + testCase.caseName, async () => {
|
|
3692
3685
|
applyProjectProviderMocks(testCase.exportName, 'next-app-component');
|
|
3693
|
-
const moduleExports = loadModuleExports();
|
|
3686
|
+
const moduleExports = await loadModuleExports();
|
|
3694
3687
|
const component = readExportValue(moduleExports, testCase.exportName);
|
|
3695
3688
|
const rendered = applyProjectProviderRender(component(testCase.props), testCase.exportName, 'next-app-component');
|
|
3696
3689
|
assertNormalizedRenderContract(rendered);
|
|
@@ -3738,9 +3731,9 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3738
3731
|
if (COMPONENT_CASES.length > 0) {
|
|
3739
3732
|
describe('react component adapter', () => {
|
|
3740
3733
|
for (const testCase of COMPONENT_CASES) {
|
|
3741
|
-
test(testCase.exportName + ' ' + testCase.caseName, () => {
|
|
3734
|
+
test(testCase.exportName + ' ' + testCase.caseName, async () => {
|
|
3742
3735
|
applyProjectProviderMocks(testCase.exportName, 'react-component');
|
|
3743
|
-
const moduleExports = loadModuleExports();
|
|
3736
|
+
const moduleExports = await loadModuleExports();
|
|
3744
3737
|
const component = readExportValue(moduleExports, testCase.exportName);
|
|
3745
3738
|
const rendered = applyProjectProviderRender(component(testCase.props), testCase.exportName, 'react-component');
|
|
3746
3739
|
assertNormalizedRenderContract(rendered);
|
|
@@ -3788,9 +3781,9 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3788
3781
|
if (HOOK_CASES.length > 0) {
|
|
3789
3782
|
describe('react hook adapter', () => {
|
|
3790
3783
|
for (const testCase of HOOK_CASES) {
|
|
3791
|
-
test(testCase.exportName + ' ' + testCase.caseName, () => {
|
|
3784
|
+
test(testCase.exportName + ' ' + testCase.caseName, async () => {
|
|
3792
3785
|
applyProjectProviderMocks(testCase.exportName, 'react-hook');
|
|
3793
|
-
const moduleExports = loadModuleExports();
|
|
3786
|
+
const moduleExports = await loadModuleExports();
|
|
3794
3787
|
const hook = readExportValue(moduleExports, testCase.exportName);
|
|
3795
3788
|
const result = hook(...testCase.args);
|
|
3796
3789
|
assertHookResultContract(result);
|
|
@@ -3810,7 +3803,7 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3810
3803
|
for (const testCase of NEXT_ROUTE_CASES) {
|
|
3811
3804
|
test(testCase.exportName + ' ' + testCase.caseName, async () => {
|
|
3812
3805
|
applyProjectProviderMocks(testCase.exportName, 'next-route-handler');
|
|
3813
|
-
const moduleExports = loadModuleExports();
|
|
3806
|
+
const moduleExports = await loadModuleExports();
|
|
3814
3807
|
const handler = readExportValue(moduleExports, testCase.exportName);
|
|
3815
3808
|
const request = createRequestFromSpec(testCase.request);
|
|
3816
3809
|
const response = await Promise.resolve(handler(request, testCase.context));
|
|
@@ -3826,7 +3819,7 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3826
3819
|
for (const testCase of ROUTE_CASES) {
|
|
3827
3820
|
test(testCase.exportName + ' ' + testCase.caseName, async () => {
|
|
3828
3821
|
applyProjectProviderMocks(testCase.exportName, 'route-handler');
|
|
3829
|
-
const moduleExports = loadModuleExports();
|
|
3822
|
+
const moduleExports = await loadModuleExports();
|
|
3830
3823
|
const handler = readExportValue(moduleExports, testCase.exportName);
|
|
3831
3824
|
const request = createRequestFromSpec(testCase.request);
|
|
3832
3825
|
const response = await Promise.resolve(handler(request, testCase.context));
|
|
@@ -3842,7 +3835,7 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3842
3835
|
for (const testCase of SERVICE_CASES) {
|
|
3843
3836
|
test(testCase.exportName + ' ' + testCase.caseName, async () => {
|
|
3844
3837
|
applyProjectProviderMocks(testCase.exportName, 'node-service');
|
|
3845
|
-
const moduleExports = loadModuleExports();
|
|
3838
|
+
const moduleExports = await loadModuleExports();
|
|
3846
3839
|
const service = readExportValue(moduleExports, testCase.exportName);
|
|
3847
3840
|
const result = await Promise.resolve(service(...testCase.args));
|
|
3848
3841
|
assertServiceResultContractShape(normalizeBehaviorValue(result));
|
|
@@ -3854,6 +3847,43 @@ describe(${JSON.stringify(suiteName)}, () => {
|
|
|
3854
3847
|
`;
|
|
3855
3848
|
}
|
|
3856
3849
|
|
|
3850
|
+
function renderGeneratedRequirePrelude({ helperImport }) {
|
|
3851
|
+
return `const path = require('path');
|
|
3852
|
+
const {
|
|
3853
|
+
listExportNames,
|
|
3854
|
+
buildModuleContract,
|
|
3855
|
+
readExportValue,
|
|
3856
|
+
normalizeBehaviorValue,
|
|
3857
|
+
normalizeRouteResult,
|
|
3858
|
+
createRequestFromSpec,
|
|
3859
|
+
assertSourceFreshness,
|
|
3860
|
+
runComponentInteractionContract,
|
|
3861
|
+
runComponentBehaviorFlowContract,
|
|
3862
|
+
runHookInteractionContract
|
|
3863
|
+
} = require(${JSON.stringify(helperImport)});`;
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
function renderGeneratedImportPrelude({ helperImport, providerImport }) {
|
|
3867
|
+
const providerLine = providerImport
|
|
3868
|
+
? `import * as PROJECT_PROVIDER_MODULE from ${JSON.stringify(providerImport)};`
|
|
3869
|
+
: 'const PROJECT_PROVIDER_MODULE = null;';
|
|
3870
|
+
|
|
3871
|
+
return `import path from 'path';
|
|
3872
|
+
import {
|
|
3873
|
+
listExportNames,
|
|
3874
|
+
buildModuleContract,
|
|
3875
|
+
readExportValue,
|
|
3876
|
+
normalizeBehaviorValue,
|
|
3877
|
+
normalizeRouteResult,
|
|
3878
|
+
createRequestFromSpec,
|
|
3879
|
+
assertSourceFreshness,
|
|
3880
|
+
runComponentInteractionContract,
|
|
3881
|
+
runComponentBehaviorFlowContract,
|
|
3882
|
+
runHookInteractionContract
|
|
3883
|
+
} from ${JSON.stringify(helperImport)};
|
|
3884
|
+
${providerLine}`;
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3857
3887
|
function flattenScenarioCases(scenarios, kind) {
|
|
3858
3888
|
const scenario = scenarios.find((candidate) => candidate.kind === kind);
|
|
3859
3889
|
return scenario && Array.isArray(scenario.cases) ? scenario.cases : [];
|