tommy351 / kubernetes-models-ts

Kubernetes models in TypeScript.
https://www.npmjs.com/package/kubernetes-models
MIT License
136 stars 36 forks source link

Some very strange scoping shenanigans in ESM #207

Open Makeshift opened 2 weeks ago

Makeshift commented 2 weeks ago

Describe the bug I've been attempting to get @kubernetes-models/crd-generate working when wrapped in an ESM project.

This involved a couple of minor patches to fix CommonJS interop:

diff --git a/dist/nullable-ref.mjs b/dist/nullable-ref.mjs
index 567d2ecffb3fdb66c569f2782a73a60d7df6a250..7658399b564aabe97e32900e60fe497de2ddc073 100644
--- a/dist/nullable-ref.mjs
+++ b/dist/nullable-ref.mjs
@@ -1,5 +1,6 @@
 import { _ } from "ajv";
-import ref from "ajv/dist/vocabularies/core/ref";
+import _ref from "ajv/dist/vocabularies/core/ref";
+const ref = _ref.default
 const keyword = {
     keyword: "nullableRef",
     schemaType: "string",
diff --git a/dist/schema.mjs b/dist/schema.mjs
index 0a7a4985bfa52180ded7d4ce3fadf48e7095a0ea..cccb316a15f7057303cfb340980d81f6efe1ac5c 100644
--- a/dist/schema.mjs
+++ b/dist/schema.mjs
@@ -4,9 +4,11 @@ import standaloneCode from "ajv/dist/standalone";
 import assert from "assert";
 import { formats } from "@kubernetes-models/validate";
 import { parse } from "@babel/parser";
-import traverse from "@babel/traverse";
+import _traverse from "@babel/traverse";
+const traverse = _traverse.default;
 import * as t from "@babel/types";
-import generate from "@babel/generator";
+import _generate from "@babel/generator";
+const generate = _generate.default;
 import { objectHash, sha256base64 } from "ohash";
 import nullableRef from "./nullable-ref.mjs";
 import pattern from "./pattern.mjs";

and a couple of very specific resolutions in package.json (the patch refers to the patch above):

  "resolutions": {
    "lodash": "npm:lodash-es@^4.17.21",
    "@kubernetes-models/generate@npm:^2.4.0": "patch:@kubernetes-models/generate@npm%3A2.4.0#~/.yarn/patches/@kubernetes-models-generate-npm-2.4.0-6b90c54d25.patch"
  },

So far, so good. I'm able to generate the code as expected:

import { generate } from '@kubernetes-models/crd-generate'

await generate({
  input: <a yaml doc>,
  outputPath: path
})

But now I've hit the most bizarre issue I've ever seen when creating an instance of a class, in this case a KEDA ClusterTriggerAuthentication (I'm aware that the KEDA spec already exists in this repo - I was generating the same thing to sanity check my wrapper):

const clusterTriggerAuth = new kedaSh.v1alpha1.ClusterTriggerAuthentication({
  metadata: {
    name: 'get-aws-auth-from-workload-service-account'
  },
  spec: {
    podIdentity: {
      provider: 'aws',
      identityOwner: 'workload'
    }
  }
})
console.log(Object.keys(clusterTriggerAuth)) // ['apiVersion', 'kind', 'metadata', 'spec', 'status']
console.log(Object.values(clusterTriggerAuth)) // [undefined, undefined, undefined, undefined, undefined]

wut.

Making this even more confusing, if I dig into model.mjs where the base model class is defined, it appears to be working fine (console logs added by me just so I have an output to show - it's visible in the debugger too):

export class Model {
    constructor(data) {
        if (data) {
            setDefinedProps(data, this);
        }
        console.log(Object.keys(this)) // ['apiVersion', 'kind', 'metadata', 'spec']
        console.log(Object.values(this)) // ['keda.sh/v1alpha1', 'ClusterTriggerAuthentication', {…}, {…}] << It has values here!
    }
    toJSON() {
        const result = {};
        setDefinedProps(this, result);
        return result;
    }
    validate() {
        // Use `setValidateFunc` to set the validate function
    }
}

alright, so the base class sets them fine. What about the constructor of the ClusterTriggerAuthentication?

/**
 * ClusterTriggerAuthentication defines how a trigger can authenticate globally
 */
export class ClusterTriggerAuthentication extends Model<IClusterTriggerAuthentication> implements IClusterTriggerAuthentication {
  "apiVersion": IClusterTriggerAuthentication["apiVersion"];
  "kind": IClusterTriggerAuthentication["kind"];
  "metadata"?: IClusterTriggerAuthentication["metadata"];
  "spec": IClusterTriggerAuthentication["spec"];
  "status"?: IClusterTriggerAuthentication["status"];

static apiVersion: IClusterTriggerAuthentication["apiVersion"] = "keda.sh/v1alpha1";
static kind: IClusterTriggerAuthentication["kind"] = "ClusterTriggerAuthentication";
static is = createTypeMetaGuard<IClusterTriggerAuthentication>(ClusterTriggerAuthentication);

  constructor(data?: ModelData<IClusterTriggerAuthentication>) {
  console.log('Input:', data) // Input: {metadata: {…}, spec: {…}} (correct input data)
  super({
    apiVersion: ClusterTriggerAuthentication.apiVersion,
    kind: ClusterTriggerAuthentication.kind,
    ...data
  } as IClusterTriggerAuthentication);
  console.log('Output:', Object.values(this)) // Output: (5) [undefined, undefined, undefined, undefined, undefined] wut?!
}
}

Now I'm not going to lie, I'm quite tired and may be being a complete numpty, and am definitely using your library in an unintended way so you have no reason to support this use-case.

But if you can see an obvious error in what I'm doing, please point it out. Does ESM import/export do something crazy with setDefinedProps? Am I just missing something really obvious because I'm tired?

If you're willing to take a look at this but need a reproduction repo, let me know and I'll see if I can. I do totally understand if this is way out of scope for your project, though.

Either way, I will get this to work, but perhaps I'll just call out to the CLI version rather than calling generate directly in my code...