Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Uncaught DOMException: Failed to execute 'define' on 'CustomElementRegistry': "n" is not a valid custom element name #163

Closed
mjacobus opened this issue Jan 26, 2022 · 5 comments

Comments

@mjacobus
Copy link

mjacobus commented Jan 26, 2022

Hey there 👋🏻

I created a couple of custom elements using catalyst and it worked just fine, once a managed to get decorators to work with Rails 6 webpacker. However, once I deployed it to heroku, I got the following error in the console:

register.js:15 Uncaught DOMException: Failed to execute 'define' on 'CustomElementRegistry': "n" is not a valid custom element name
    at https://my-app.herokuapp.com/packs/js/application-55c5e8e27a6ea838e36d.js:2:217687
    at O (https://my-app.herokuapp.com/packs/js/application-55c5e8e27a6ea838e36d.js:2:217700)
    at Module.<anonymous> (https://my-app.herokuapp.com/packs/js/application-55c5e8e27a6ea838e36d.js:2:241463)
    at n (https://my-app.herokuapp.com/packs/js/application-55c5e8e27a6ea838e36d.js:2:110)
    at https://my-app.herokuapp.com/packs/js/application-55c5e8e27a6ea838e36d.js:2:911
    at https://my-app.herokuapp.com/packs/js/application-55c5e8e27a6ea838e36d.js:2:921
(anonymous) @ register.js:15
O @ controller.js:25
(anonymous) @ nearby_facilities_inputs.ts:4
n @ bootstrap:19
(anonymous) @ bootstrap:83
(anonymous) @ application-55c5e8e27a6ea838e36d.js:2

That error broke the entire application.js. My nearby_facility_inputs.ts is this:

import { controller, targets, target, attr } from "@github/catalyst";
import { render, html } from "@github/jtml";

@controller
export class NearbyFacilitiesElement extends HTMLElement {
  @targets inputs: NearbyFacilityElement;
  @target addButton: HTMLButtonElement;

  addFacility(e) {
    e.preventDefault();

    const el = document.createElement("nearby-facility");

    const index = new Date().getTime();

    ["name", "distance", "icon"].forEach((field) => {
      el.setAttribute(
        `data-input-name-for-${field}`,
        this.getAttribute(`data-input-name-for-${field}`).replace(
          /{index}/,
          index
        )
      );
      el.setAttribute(
        `data-placeholder-for-${field}`,
        this.getAttribute(`data-placeholder-for-${field}`).replace(
          /{index}/,
          index
        )
      );
    });

    el.setAttribute(
      "data-remove-label",
      this.getAttribute("data-remove-label")
    );
    this.appendChild(el);
  }
}

@controller
export class NearbyFacilityElement extends HTMLElement {
  @attr name = "";
  @attr distance = "";
  @attr icon = "";
  @target iconElement: HTMLElement;

  connectedCallback() {
    this.render();
  }

  placeholderFor(field: string) {
    return this.getAttribute(`data-placeholder-for-${field}`) || field;
  }

  inputNameFor(field) {
    return this.getAttribute(`data-input-name-for-${field}`) || field;
  }

  render() {
    const template = html`
      <div class="input-group mt-3">
        <input
          name="${this.inputNameFor("name")}"
          placeholder="${this.placeholderFor("name")}"
          class="form-control"
          value="${this.getAttribute("data-name")}"
        />
        <input
          name="${this.inputNameFor("distance")}"
          placeholder="${this.placeholderFor("distance")}"
          class="form-control"
          value="${this.getAttribute("data-distance")}"
        />
        <input
          name="${this.inputNameFor("icon")}"
          placeholder="${this.placeholderFor("icon")}"
          class="form-control"
          onkeyup="${this.updateIcon.bind(this)}"
          value="${this.getAttribute("data-icon")}"
        />
        <div class="input-group-text" style="width: 40px;">
          <i
            data-target="nearby-facility.iconElement"
            class="bi bi-${this.getAttribute("data-icon")}"
          ></i>
        </div>
        <button onclick="${this.remove.bind(this)}" class="btn btn-danger">
          ${this.removeLabel()}
        </button>
      </div>
    `;
    render(template, this);
  }

  updateIcon(e) {
    const value = e.target.value;
    this.iconElement.setAttribute("class", `bi bi-${value}`);
  }

  removeLabel() {
    return this.getAttribute("data-remove-label") || "Remove";
  }

  remove() {
    this.parentElement.removeChild(this);
  }
}

I am not sure this is a catalyst specific error. I suppose not. I think this is a transpilation error, but I am unfamiliar with the whole webpack babel thing I decided to ask for some directions here.

Also, I am wondering how could I make those custom elements not break the entire application.js - perhaps I could wrap some code in a try/catch?

Thank you in advance ❤️

@koddsson
Copy link
Contributor

This is documented in "Build considerations" from https://github.github.io/catalyst/guide/you-will-need/

When using build tools, some JavaScript minifiers modify the class name that Catalyst relies on. You know you have an issue if you encounter the error "c" is not a valid custom element name.

A best practice is to allow class names that end with Element. For instance, for Terser, you can use the following config: { keep_classnames: /Element$/ }

What minifier are you using in your production builds?

@mjacobus
Copy link
Author

mjacobus commented Jan 26, 2022

Oh... I see. That does make sense! Thank you so much for that link.

What minifier are you using in your production builds?

I don't know yet. I don't understand the whole webpack/build config. I just used what rails gave me. I will figure out and come back here soon.

This is my package.json.

{
  "name": "Project",
  "private": true,
  "dependencies": {
    "@babel/preset-typescript": "^7.16.7",
    "@babel/plugin-proposal-decorators": "^7.16.7",
    "@github/catalyst": "^1.1.4",
    "@github/jtml": "^0.4.0",
    "@github/template-parts": "^0.3.2",
    "@popperjs/core": "^2.9.2",
    "@rails/actioncable": "^6.0.0",
    "@rails/activestorage": "^6.0.0",
    "@rails/ujs": "^6.0.0",
    "@rails/webpacker": "5.4.3",
    "bootstrap": "^5.0.0",
    "delegated-events": "^1.1.2",
    "jquery": "^3.6.0",
    "minimist": "^1.2.5",
    "prettier": "^2.3.1",
    "selector-observer": "^2.1.6",
    "turbolinks": "^5.2.0",
    "typescript": "^4.5.5",
    "webpack-cli": "^4.9.2"
  },
  "version": "0.1.0",
  "devDependencies": {
    "@webpack-cli/serve": "^1.6.1",
    "jest": "^27.1.0",
    "webpack-dev-server": "^4.7.3"
  },
  "engines": {
    "node": "14.18.3"
  }
}

@koddsson
Copy link
Contributor

This is my package.json.

Do you have any configuration for webpack? I don't actually know how webpacker works and how it's configured in Rails 6 😬

@mjacobus
Copy link
Author

I think it uses Terser as a minifier, but it appears my config changes won't affect the build. I tried setting keep_classnames to /Element$/ and it did not work. I also tried removing the Terser plugin from the chain and it also did not work.

My webpack config
ConfigObject {
  mode: 'production',
  output: {
    filename: 'js/[name]-[contenthash].js',
    chunkFilename: 'js/[name]-[contenthash].chunk.js',
    hotUpdateChunkFilename: 'js/[id]-[hash].hot-update.js',
    path: '/Volumes/Data/Projects/my-app/public/packs',
    publicPath: '/packs/'
  },
  resolve: {
    extensions: [
      '.tsx',         '.ts',
      '.mjs',         '.js',
      '.sass',        '.scss',
      '.css',         '.module.sass',
      '.module.scss', '.module.css',
      '.png',         '.svg',
      '.gif',         '.jpeg',
      '.jpg'
    ],
    plugins: [
      {
        apply: [Function: nothing],
        makePlugin: [Function (anonymous)],
        moduleLoader: [Function (anonymous)],
        topLevelLoader: { apply: [Function: nothing] },
        bind: [Function (anonymous)],
        tsLoaderOptions: [Function (anonymous)],
        forkTsCheckerOptions: [Function (anonymous)]
      }
    ],
    modules: [
      '/Volumes/Data/Projects/my-app/app/javascript',
      '/Volumes/Data/Projects/my-app/app/components',
      'node_modules'
    ]
  },
  resolveLoader: {
    modules: [ 'node_modules' ],
    plugins: [ { apply: [Function: nothing] } ]
  },
  node: {
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty'
  },
  devtool: 'source-map',
  stats: 'normal',
  bail: true,
  optimization: {
    minimizer: [
      TerserPlugin {
        options: {
          test: /\.[cm]?js(\?.*)?$/i,
          extractComments: true,
          sourceMap: true,
          cache: true,
          cacheKeys: [Function: cacheKeys],
          parallel: true,
          include: undefined,
          exclude: undefined,
          minify: undefined,
          terserOptions: {
            parse: { ecma: 8 },
            compress: { ecma: 5, warnings: false, comparisons: false },
            mangle: { safari10: true },
            output: { ecma: 5, comments: false, ascii_only: true }
          }
        }
      }
    ]
  },
  entry: {
    application: '/Volumes/Data/Projects/my-app/app/javascript/packs/application.js',
    hello_typescript: '/Volumes/Data/Projects/my-app/app/javascript/packs/hello_typescript.ts',
    site: '/Volumes/Data/Projects/my-app/app/javascript/packs/site.js'
  },
  module: {
    strictExportPresence: true,
    rules: [
      {
        test: /(.jpg|.jpeg|.png|.gif|.tiff|.ico|.svg|.eot|.otf|.ttf|.woff|.woff2)$/i,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: [Function: name],
              esModule: false,
              context: 'app/javascript'
            }
          }
        ]
      },
      {
        test: /\.(css)$/i,
        use: [
          '/Volumes/Data/Projects/my-app/node_modules/mini-css-extract-plugin/dist/loader.js',
          {
            loader: 'css-loader',
            options: { sourceMap: true, importLoaders: 2, modules: false }
          },
          {
            loader: 'postcss-loader',
            options: {
              config: { path: '/Volumes/Data/Projects/my-app' },
              sourceMap: true
            }
          }
        ],
        sideEffects: true,
        exclude: /\.module\.[a-z] $/
      },
      {
        test: /\.(scss|sass)(\.erb)?$/i,
        use: [
          '/Volumes/Data/Projects/my-app/node_modules/mini-css-extract-plugin/dist/loader.js',
          {
            loader: 'css-loader',
            options: { sourceMap: true, importLoaders: 2, modules: false }
          },
          {
            loader: 'postcss-loader',
            options: {
              config: { path: '/Volumes/Data/Projects/my-app' },
              sourceMap: true
            }
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true,
              implementation: {
                load: [Function (anonymous)],
                compile: [Function: sass.compile],
                compileString: [Function: sass.compileString],
                compileAsync: [Function: sass.compileAsync],
                compileStringAsync: [Function: sass.compileStringAsync],
                Value: [Function: Value0],
                SassBoolean: [Function: sass.SassBoolean],
                SassArgumentList: [Function: sass.SassArgumentList],
                SassColor: [Function: sass.SassColor],
                SassFunction: [Function: sass.SassFunction],
                SassList: [Function: sass.SassList],
                SassMap: [Function: sass.SassMap],
                SassNumber: [Function: sass.SassNumber],
                SassString: [Function: sass.SassString],
                sassNull: null,
                sassTrue: true,
                sassFalse: false,
                Exception: [class sass.Exception extends Error],
                Logger: {
                  silent: {
                    warn: [Function: sass.Logger.silent.warn],
                    debug: [Function: sass.Logger.silent.debug]
                  }
                },
                info: 'dart-sass\t1.49.0\t(Sass Compiler)\t[Dart]\n'  
                  'dart2js\t2.15.1\t(Dart Compiler)\t[Dart]',
                render: [Function: sass.render],
                renderSync: [Function: sass.renderSync],
                types: {
                  Boolean: [Function: sass.types.Boolean] {
                    TRUE: true,
                    FALSE: false
                  },
                  Color: [Function: sass.types.Color],
                  List: [Function: sass.types.List],
                  Map: [Function: sass.types.Map],
                  Null: [Function: sass.types.Null] { NULL: null },
                  Number: [Function: sass.types.Number],
                  String: [Function: sass.types.String],
                  Error: [Function: Error] { stackTraceLimit: 10 }
                },
                NULL: null,
                TRUE: true,
                FALSE: false,
                cli_pkg_main_0_: <ref *1> [Function (anonymous)] {
                  '___dart__$dart_dartClosure_ZxYxX_0_': _wrapMain_closure0 {
                    main: StaticClosure {
                      '$initialize': [Function: StaticClosure],
                      constructor: [Function: static_tear_off],
                      '$_name': 'main0',
                      '$_target': [Function: main0],
                      '$static_name': 'main0',
                      '$signature': 604,
                      'call$1': [Function: main0],
                      'call*': [Function: main0],
                      '$requiredArgCount': 1,
                      '$defaultValues': null
                    },
                    '$dart_jsFunction': [Circular *1]
                  }
                }
              },
              sassOptions: { includePaths: [ 'app/components' ] }
            }
          }
        ],
        sideEffects: true,
        exclude: /\.module\.[a-z] $/
      },
      {
        test: /\.(css)$/i,
        use: [
          '/Volumes/Data/Projects/my-app/node_modules/mini-css-extract-plugin/dist/loader.js',
          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
              importLoaders: 2,
              modules: { localIdentName: '[name]__[local]___[hash:base64:5]' }
            }
          },
          {
            loader: 'postcss-loader',
            options: {
              config: { path: '/Volumes/Data/Projects/my-app' },
              sourceMap: true
            }
          }
        ],
        sideEffects: false,
        include: /\.module\.[a-z] $/
      },
      {
        test: /\.(scss|sass)$/i,
        use: [
          '/Volumes/Data/Projects/my-app/node_modules/mini-css-extract-plugin/dist/loader.js',
          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
              importLoaders: 2,
              modules: { localIdentName: '[name]__[local]___[hash:base64:5]' }
            }
          },
          {
            loader: 'postcss-loader',
            options: {
              config: { path: '/Volumes/Data/Projects/my-app' },
              sourceMap: true
            }
          },
          { loader: 'sass-loader', options: { sourceMap: true } }
        ],
        sideEffects: false,
        include: /\.module\.[a-z] $/
      },
      {
        test: /\.(js|mjs)$/,
        include: /node_modules/,
        exclude: /(?:@?babel(?:\/|\\{1,2}|-). )|regenerator-runtime|core-js|^webpack$|^webpack-assets-manifest$|^webpack-cli$|^webpack-sources$|^@rails\/webpacker$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              babelrc: false,
              presets: [ [ '@babel/preset-env', { modules: false } ] ],
              cacheDirectory: true,
              cacheCompression: true,
              compact: false,
              sourceMaps: false
            }
          }
        ]
      },
      {
        test: /\.(js|jsx|mjs|ts|tsx)?(\.erb)?$/,
        include: [
          '/Volumes/Data/Projects/my-app/app/javascript',
          '/Volumes/Data/Projects/my-app/app/components'
        ],
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true,
              cacheCompression: true,
              compact: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    ProvidePlugin {
      definitions: {
        '$': 'jquery',
        jQuery: 'jquery',
        jquery: 'jquery',
        Popper: [ 'popper.js', 'default' ]
      }
    },
    EnvironmentPlugin {
      keys: [
        ...
      ],
      defaultValues: {
        ...
        WEBPACKER_CONFIG: '/Volumes/Data/Projects/my-app/config/webpacker.yml',
      }
    },
    CaseSensitivePathsPlugin {
      options: {},
      logger: Object [console] {
        log: [Function: log],
        warn: [Function: warn],
        dir: [Function: dir],
        time: [Function: time],
        timeEnd: [Function: timeEnd],
        timeLog: [Function: timeLog],
        trace: [Function: trace],
        assert: [Function: assert],
        clear: [Function: clear],
        count: [Function: count],
        countReset: [Function: countReset],
        group: [Function: group],
        groupEnd: [Function: groupEnd],
        table: [Function: table],
        debug: [Function: debug],
        info: [Function: info],
        dirxml: [Function: dirxml],
        error: [Function: error],
        groupCollapsed: [Function: groupCollapsed],
        Console: [Function: Console],
        profile: [Function: profile],
        profileEnd: [Function: profileEnd],
        timeStamp: [Function: timeStamp],
        context: [Function: context]
      },
      pathCache: Map(0) {},
      fsOperations: 0,
      primed: false
    },
    MiniCssExtractPlugin {
      options: {
        filename: 'css/[name]-[contenthash:8].css',
        moduleFilename: [Function: moduleFilename],
        ignoreOrder: false,
        chunkFilename: 'css/[name]-[contenthash:8].chunk.css'
      }
    },
    WebpackAssetsManifest {
      hooks: {
        apply: SyncHook {
          _args: [ 'manifest' ],
          taps: [],
          interceptors: [],
          call: [Function: lazyCompileHook],
          promise: [Function: lazyCompileHook],
          callAsync: [Function: lazyCompileHook],
          _x: undefined
        },
        customize: SyncWaterfallHook {
          _args: [ 'entry', 'original', 'manifest', 'asset' ],
          taps: [],
          interceptors: [],
          call: [Function: lazyCompileHook],
          promise: [Function: lazyCompileHook],
          callAsync: [Function: lazyCompileHook],
          _x: undefined
        },
        transform: SyncWaterfallHook {
          _args: [ 'assets', 'manifest' ],
          taps: [
            {
              type: 'sync',
              fn: [Function (anonymous)],
              name: 'WebpackAssetsManifest'
            }
          ],
          interceptors: [],
          call: [Function: anonymous],
          promise: [Function: lazyCompileHook],
          callAsync: [Function: lazyCompileHook],
          _x: [ [Function (anonymous)] ]
        },
        done: SyncHook {
          _args: [ 'manifest', 'stats' ],
          taps: [],
          interceptors: [],
          call: [Function: lazyCompileHook],
          promise: [Function: lazyCompileHook],
          callAsync: [Function: lazyCompileHook],
          _x: undefined
        },
        options: SyncWaterfallHook {
          _args: [ 'options' ],
          taps: [],
          interceptors: [],
          call: [Function: lazyCompileHook],
          promise: [Function: lazyCompileHook],
          callAsync: [Function: lazyCompileHook],
          _x: undefined
        },
        afterOptions: SyncHook {
          _args: [ 'options' ],
          taps: [
            {
              type: 'sync',
              fn: [Function (anonymous)],
              name: 'WebpackAssetsManifest'
            }
          ],
          interceptors: [],
          call: [Function: lazyCompileHook],
          promise: [Function: lazyCompileHook],
          callAsync: [Function: lazyCompileHook],
          _x: undefined
        }
      },
      options: {
        assets: [Object: null prototype] {},
        output: 'manifest.json',
        replacer: null,
        space: 2,
        writeToDisk: true,
        fileExtRegex: /\.\w{2,4}\.(?:map|gz)$|\.\w $/i,
        sortManifest: true,
        merge: false,
        publicPath: '/packs/',
        apply: null,
        customize: null,
        transform: null,
        done: null,
        entrypoints: true,
        entrypointsKey: 'entrypoints',
        integrity: false,
        integrityHashes: [ 'sha256', 'sha384', 'sha512' ],
        integrityPropertyName: 'integrity'
      },
      assets: [Object: null prototype] {},
      assetNames: Map(0) {},
      currentAsset: null,
      compiler: null,
      stats: null,
      hmrRegex: null,
      [Symbol(isMerging)]: false
    },
    CompressionPlugin {
      options: {
        test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
        include: undefined,
        exclude: undefined,
        cache: true,
        algorithm: 'gzip',
        compressionOptions: {},
        filename: '[path].gz[query]',
        threshold: 0,
        minRatio: 0.8,
        deleteOriginalAssets: false
      },
      algorithm: [Function: asyncBufferWrapper],
      compressionOptions: { level: 9 }
    },
    CompressionPlugin {
      options: {
        test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
        include: undefined,
        exclude: undefined,
        cache: true,
        algorithm: 'brotliCompress',
        compressionOptions: {},
        filename: '[path].br[query]',
        threshold: 0,
        minRatio: 0.8,
        deleteOriginalAssets: false
      },
      algorithm: [Function: asyncBufferWrapper],
      compressionOptions: { level: 9 }
    },
    OptimizeCssAssetsWebpackPlugin {
      pluginDescriptor: { name: 'OptimizeCssAssetsWebpackPlugin' },
      options: {
        assetProcessors: [
          {
            phase: 'compilation.optimize-chunk-assets',
            regExp: /\.css(\?.*)?$/i,
            processor: [Function: processor]
          }
        ],
        canPrint: undefined,
        assetNameRegExp: /\.css(\?.*)?$/i,
        cssProcessor: [Function: creator] { process: [Function (anonymous)] },
        cssProcessorOptions: {},
        cssProcessorPluginOptions: {}
      },
      phaseAssetProcessors: {
        'compilation.optimize-chunk-assets': [
          {
            phase: 'compilation.optimize-chunk-assets',
            regExp: /\.css(\?.*)?$/i,
            processor: [Function: processor]
          }
        ],
        'compilation.optimize-assets': [],
        emit: []
      },
      deleteAssetsMap: {}
    }
  ]
}
My Babel config
module.exports = function (api) {
  var validEnv = ["development", "test", "production"];
  var currentEnv = api.env();
  var isDevelopmentEnv = api.env("development");
  var isProductionEnv = api.env("production");
  var isTestEnv = api.env("test");

  if (!validEnv.includes(currentEnv)) {
    throw new Error(
      "Please specify a valid `NODE_ENV` or "  
        '`BABEL_ENV` environment variables. Valid values are "development", '  
        '"test", and "production". Instead, received: '  
        JSON.stringify(currentEnv)  
        "."
    );
  }

  return {
    presets: [
      isTestEnv && [
        "@babel/preset-env",
        {
          targets: {
            node: "current",
          },
        },
      ],
      (isProductionEnv || isDevelopmentEnv) && [
        "@babel/preset-env",
        {
          forceAllTransforms: true,
          useBuiltIns: "entry",
          corejs: 3,
          modules: false,
          exclude: ["transform-typeof-symbol"],
        },
      ],
      ["@babel/preset-typescript", { allExtensions: true, isTSX: true }],
    ].filter(Boolean),
    plugins: [
      "babel-plugin-macros",
      "@babel/plugin-syntax-dynamic-import",
      isTestEnv && "babel-plugin-dynamic-import-node",
      "@babel/plugin-transform-destructuring",
      [
        "@babel/plugin-proposal-decorators",
        {
          legacy: true,
        },
      ],
      [
        "@babel/plugin-proposal-class-properties",
        {
          loose: true,
        },
      ],
      [
        "@babel/plugin-proposal-private-property-in-object",
        {
          loose: true,
        },
      ],
      [
        "@babel/plugin-proposal-private-methods",
        {
          loose: true,
        },
      ],
      [
        "@babel/plugin-proposal-object-rest-spread",
        {
          useBuiltIns: true,
        },
      ],
      [
        "@babel/plugin-transform-runtime",
        {
          helpers: false,
          regenerator: true,
          corejs: false,
        },
      ],
      [
        "@babel/plugin-transform-regenerator",
        {
          async: false,
        },
      ],
    ].filter(Boolean),
  };
};

I think this is a @rails/webpacker specific issue now. I will try to figure it out.

Thank you for your help identifying the problem @koddsson!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants