Preact-cli: "preact build" try to load an (3rd lib) un-exist ts file

Created on 8 Nov 2020  路  6Comments  路  Source: preactjs/preact-cli


Do you want to request a _feature_ or report a _bug_?
bug (maybe)

What is the current behaviour?
I using a 3rd parity library, and preact build fail due to trying to load an un-exist ts file

  • @justinribeiro/lite-youtube
  • @justinribeiro/lite-youtube's file structure, package.json and lite-youtube.js are listed in below

    • package.json's main and module are indicate to lite-youtube.js

    • but preact-cli try to load ts file (lite-youtube.ts)

problem can be solved by --no-prerender flag, but is it normal ?

If the current behaviour is a bug, please provide the steps to reproduce.

steps to reproduce

  1. preact create Default preact-cli-build-issue
  2. cd preact-cli-build-issue
  3. npm i @justinribeiro/lite-youtube
  4. import @justinribeiro/lite-youtube and write some code (https://github.com/flameddd/preact-cli-build-issue/commit/a1bb98f837dd91ef54ce440a25856b408ce289b4)
  5. npm run build

I had create repo to reproduce this

What is the expected behaviour?
npm run build success
(according @rschristian explanation) correct error message

Please mention other relevant information.

@justinribeiro/lite-youtube file structure

.
./lite-youtube.d.ts
./LICENSE
./lite-youtube.js.map
./README.md
./package.json
./lite-youtube.js

@justinribeiro/lite-youtube package.json


Click to expand package.json

{
  "_from": "@justinribeiro/lite-youtube",
  "_id": "@justinribeiro/[email protected]",
  "_inBundle": false,
  "_integrity": "sha512-IgcpHnovzZGxU4Ec+0c7sSLhrJWflvYliQUmdcwBgyVkGw0ZL9Y8IU/m09NPk9EzIk2HAOWUGLywTVpB785egA==",
  "_location": "/@justinribeiro/lite-youtube",
  "_phantomChildren": {},
  "_requested": {
    "type": "tag",
    "registry": true,
    "raw": "@justinribeiro/lite-youtube",
    "name": "@justinribeiro/lite-youtube",
    "escapedName": "@justinribeiro%2flite-youtube",
    "scope": "@justinribeiro",
    "rawSpec": "",
    "saveSpec": null,
    "fetchSpec": "latest"
  },
  "_requiredBy": [
    "#USER",
    "/"
  ],
  "_resolved": "https://registry.npmjs.org/@justinribeiro/lite-youtube/-/lite-youtube-0.9.1.tgz",
  "_shasum": "c9f83861daad361d58de76b2a5e078de6fe6b751",
  "_spec": "@justinribeiro/lite-youtube",
  "_where": "/Users/flameddd/program/preact-cli-build-issue",
  "author": {
    "name": "Justin Ribeiro",
    "email": "[email protected]"
  },
  "bugs": {
    "url": "https://github.com/justinribeiro/lite-youtube/issues"
  },
  "bundleDependencies": false,
  "deprecated": false,
  "description": "A web component that loads YouTube embed iframes faster. ShadowDom based version of Paul Irish' concept.",
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^2.29.0",
    "@typescript-eslint/parser": "^2.29.0",
    "eslint": "^6.8.0",
    "eslint-config-google": "^0.14.0",
    "eslint-config-prettier": "^6.10.0",
    "eslint-plugin-html": "^6.0.0",
    "eslint-plugin-lit": "^1.2.0",
    "prettier": "^2.0.0",
    "typescript": "^3.8.0"
  },
  "files": [
    "lite-youtube.d.ts",
    "lite-youtube.js",
    "lite-youtube.js.map"
  ],
  "homepage": "https://github.com/justinribeiro/lite-youtube#readme",
  "keywords": [
    "web components",
    "youtube"
  ],
  "license": "MIT",
  "main": "lite-youtube.js",
  "module": "lite-youtube.js",
  "name": "@justinribeiro/lite-youtube",
  "repository": {
    "type": "git",
    "url": "git+ssh://[email protected]/justinribeiro/lite-youtube.git"
  },
  "scripts": {
    "build": "tsc --project tsconfig.json",
    "lint": "npm run lint:eslint && npm run lint:prettier",
    "lint:eslint": "eslint *.ts --ignore-path .gitignore",
    "lint:prettier": "prettier --check *.ts --ignore-path .gitignore",
    "prepublishOnly": "npm run build"
  },
  "types": "lite-youtube.d.ts",
  "version": "0.9.1"
}

@justinribeiro/lite-youtube lite-youtube.js


Click to expand lite-youtube.js

/**
 *
 * The shadowDom / Intersection Observer version of Paul's concept:
 * https://github.com/paulirish/lite-youtube-embed
 *
 * A lightweight YouTube embed. Still should feel the same to the user, just
 * MUCH faster to initialize and paint.
 *
 * Thx to these as the inspiration
 *   https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html
 *   https://autoplay-youtube-player.glitch.me/
 *
 * Once built it, I also found these (馃憤馃憤):
 *   https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube
 *   https://github.com/Daugilas/lazyYT https://github.com/vb/lazyframe
 */
export class LiteYTEmbed extends HTMLElement {
    constructor() {
        super();
        this.iframeLoaded = false;
        this.setupDom();
    }
    static get observedAttributes() {
        return ['videoid'];
    }
    connectedCallback() {
        this.addEventListener('pointerover', LiteYTEmbed.warmConnections, {
            once: true,
        });
        this.addEventListener('click', () => this.addIframe());
    }
    get videoId() {
        return encodeURIComponent(this.getAttribute('videoid') || '');
    }
    set videoId(id) {
        this.setAttribute('videoid', id);
    }
    get videoTitle() {
        return this.getAttribute('videotitle') || 'Video';
    }
    set videoTitle(title) {
        this.setAttribute('videotitle', title);
    }
    get videoPlay() {
        return this.getAttribute('videoPlay') || 'Play';
    }
    set videoPlay(name) {
        this.setAttribute('videoPlay', name);
    }
    get videoStartAt() {
        return Number(this.getAttribute('videoStartAt') || '0');
    }
    set videoStartAt(time) {
        this.setAttribute('videoStartAt', String(time));
    }
    get autoLoad() {
        return this.hasAttribute('autoload');
    }
    set autoLoad(value) {
        if (value) {
            this.setAttribute('autoload', '');
        }
        else {
            this.removeAttribute('autoload');
        }
    }
    get params() {
        return `start=${this.videoStartAt}&${this.getAttribute('params')}`;
    }
    /**
     * Define our shadowDOM for the component
     */
    setupDom() {
        const shadowDom = this.attachShadow({ mode: 'open' });
        shadowDom.innerHTML = `
      <style>
        :host {
          contain: content;
          display: block;
          position: relative;
          width: 100%;
          padding-bottom: calc(100% / (16 / 9));
        }

        #frame, #fallbackPlaceholder, iframe {
          position: absolute;
          width: 100%;
          height: 100%;
        }

        #frame {
          cursor: pointer;
        }

        #fallbackPlaceholder {
          object-fit: cover;
        }

        #frame::before {
          content: '';
          display: block;
          position: absolute;
          top: 0;
          background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);
          background-position: top;
          background-repeat: repeat-x;
          height: 60px;
          padding-bottom: 50px;
          width: 100%;
          transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
          z-index: 1;
        }
        /* play button */
        .lty-playbtn {
          width: 70px;
          height: 46px;
          background-color: #212121;
          z-index: 1;
          opacity: 0.8;
          border-radius: 14%; /* TODO: Consider replacing this with YT's actual svg. Eh. */
          transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
          border: 0;
        }
        #frame:hover .lty-playbtn {
          background-color: #f00;
          opacity: 1;
        }
        /* play button triangle */
        .lty-playbtn:before {
          content: '';
          border-style: solid;
          border-width: 11px 0 11px 19px;
          border-color: transparent transparent transparent #fff;
        }
        .lty-playbtn,
        .lty-playbtn:before {
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate3d(-50%, -50%, 0);
        }

        /* Post-click styles */
        .lyt-activated {
          cursor: unset;
        }

        #frame.lyt-activated::before,
        .lyt-activated .lty-playbtn {
          display: none;
        }
      </style>
      <div id="frame">
        <picture>
          <source id="webpPlaceholder" type="image/webp">
          <source id="jpegPlaceholder" type="image/jpeg">
          <img id="fallbackPlaceholder" referrerpolicy="origin">
        </picture>
        <button class="lty-playbtn"></button>
      </div>
    `;
        this.domRefFrame = this.shadowRoot.querySelector('#frame');
        this.domRefImg = {
            fallback: this.shadowRoot.querySelector('#fallbackPlaceholder'),
            webp: this.shadowRoot.querySelector('#webpPlaceholder'),
            jpeg: this.shadowRoot.querySelector('#jpegPlaceholder'),
        };
        this.domRefPlayButton = this.shadowRoot.querySelector('.lty-playbtn');
    }
    /**
     * Parse our attributes and fire up some placeholders
     */
    setupComponent() {
        this.initImagePlaceholder();
        this.domRefPlayButton.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`);
        this.setAttribute('title', `${this.videoPlay}: ${this.videoTitle}`);
        if (this.autoLoad) {
            this.initIntersectionObserver();
        }
    }
    /**
     * Lifecycle method that we use to listen for attribute changes to period
     * @param {*} name
     * @param {*} oldVal
     * @param {*} newVal
     */
    attributeChangedCallback(name, oldVal, newVal) {
        switch (name) {
            case 'videoid': {
                if (oldVal !== newVal) {
                    this.setupComponent();
                    // if we have a previous iframe, remove it and the activated class
                    if (this.domRefFrame.classList.contains('lyt-activated')) {
                        this.domRefFrame.classList.remove('lyt-activated');
                        this.shadowRoot.querySelector('iframe').remove();
                        this.iframeLoaded = false;
                    }
                }
                break;
            }
            default:
                break;
        }
    }
    /**
     * Inject the iframe into the component body
     */
    addIframe() {
        if (!this.iframeLoaded) {
            const iframeHTML = `
<iframe frameborder="0"
  allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen
  src="https://www.youtube.com/embed/${this.videoId}?autoplay=1&${this.params}"
></iframe>`;
            this.domRefFrame.insertAdjacentHTML('beforeend', iframeHTML);
            this.domRefFrame.classList.add('lyt-activated');
            this.iframeLoaded = true;
        }
    }
    /**
     * Setup the placeholder image for the component
     */
    initImagePlaceholder() {
        // we don't know which image type to preload, so warm the connection
        LiteYTEmbed.addPrefetch('preconnect', 'https://i.ytimg.com/');
        const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/hqdefault.webp`;
        const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`;
        this.domRefImg.webp.srcset = posterUrlWebp;
        this.domRefImg.jpeg.srcset = posterUrlJpeg;
        this.domRefImg.fallback.src = posterUrlJpeg;
        this.domRefImg.fallback.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`);
        this.domRefImg.fallback.setAttribute('alt', `${this.videoPlay}: ${this.videoTitle}`);
    }
    /**
     * Setup the Intersection Observer to load the iframe when scrolled into view
     */
    initIntersectionObserver() {
        if ('IntersectionObserver' in window &&
            'IntersectionObserverEntry' in window) {
            const options = {
                root: null,
                rootMargin: '0px',
                threshold: 0,
            };
            const observer = new IntersectionObserver((entries, observer) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting && !this.iframeLoaded) {
                        LiteYTEmbed.warmConnections();
                        this.addIframe();
                        observer.unobserve(this);
                    }
                });
            }, options);
            observer.observe(this);
        }
    }
    /**
     * Add a <link rel={preload | preconnect} ...> to the head
     * @param {*} kind
     * @param {*} url
     * @param {*} as
     */
    static addPrefetch(kind, url, as) {
        const linkElem = document.createElement('link');
        linkElem.rel = kind;
        linkElem.href = url;
        if (as) {
            linkElem.as = as;
        }
        linkElem.crossOrigin = 'true';
        document.head.append(linkElem);
    }
    /**
     * Begin preconnecting to warm up the iframe load Since the embed's netwok
     * requests load within its iframe, preload/prefetch'ing them outside the
     * iframe will only cause double-downloads. So, the best we can do is warm up
     * a few connections to origins that are in the critical path.
     *
     * Maybe `<link rel=preload as=document>` would work, but it's unsupported:
     * http://crbug.com/593267 But TBH, I don't think it'll happen soon with Site
     * Isolation and split caches adding serious complexity.
     */
    static warmConnections() {
        if (LiteYTEmbed.preconnected)
            return;
        // Host that YT uses to serve JS needed by player, per amp-youtube
        LiteYTEmbed.addPrefetch('preconnect', 'https://s.ytimg.com');
        // The iframe document and most of its subresources come right off
        // youtube.com
        LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube.com');
        // The botguard script is fetched off from google.com
        LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com');
        // TODO: Not certain if these ad related domains are in the critical path.
        // Could verify with domain-specific throttling.
        LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net');
        LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net');
        LiteYTEmbed.preconnected = true;
    }
}
LiteYTEmbed.preconnected = false;
// Register custom element
customElements.define('lite-youtube', LiteYTEmbed);
//# sourceMappingURL=lite-youtube.js.map

Please paste the results of preact info here.

preact info

Environment Info:
  System:
    OS: macOS 10.15.2
    CPU: (4) x64 Intel(R) Core(TM) i5-4260U CPU @ 1.40GHz
  Binaries:
    Node: 10.15.1 - ~/.nvm/versions/node/v10.15.1/bin/node
    npm: 6.14.4 - ~/.nvm/versions/node/v10.15.1/bin/npm
  Browsers:
    Chrome: 86.0.4240.183
    Edge: 81.0.416.58
    Safari: 13.0.4
  npmGlobalPackages:
    preact-cli: 3.0.3

thanks for ALL preact contributors' hard work

Most helpful comment

Weird error, definitely need to take a look at that. No idea where that's coming from as the lib does look to be set up right.

However, the fact that something errored out is correct behaviour. The library you're trying to use has the following line:

export class LiteYTEmbed extends HTMLElement

Preact-CLI prerenders in Node, and HTMLElement is browser-only. This gives you a few options: opt out of prerendering as you have, or wrap the library in a window check (if (window !== undefined) { <use library here> }).

All 6 comments

Weird error, definitely need to take a look at that. No idea where that's coming from as the lib does look to be set up right.

However, the fact that something errored out is correct behaviour. The library you're trying to use has the following line:

export class LiteYTEmbed extends HTMLElement

Preact-CLI prerenders in Node, and HTMLElement is browser-only. This gives you a few options: opt out of prerendering as you have, or wrap the library in a window check (if (window !== undefined) { <use library here> }).

Yep @rschristian has the right fix.

The lite-youtube.ts error is actually correct - that is the sourcemapped error location. If you find the GitHub repo, it likely includes a .ts file by that name and you'll see line 367 is the one Ryan mentioned that relies on HTMLElement.

Ah, whoops, source map didn't occur to me. Thanks for the save! @developit

So, the process is

  1. preact build load lite-youtube.js
  2. error ocure due to HTMLElement
  3. preact build is trying to dig more information, so load lite-youtube.js.map
  4. lite-youtube.js.map's sources field is ["lite-youtube.ts"]
  5. lite-youtube.ts is unable to read (bcuz it's not exist)

am i right?

Seems this issue can be close, but I have one more question.

Does step 3 (load source map) is common behavior (for dig more information) ?
Does other framework or bundler will do this TOO ? (any reference ?)

I did some google

  • Seems lite-youtube.js.map's sources field indicate to TS file is RIGHT.
  • Other famous library (build by TS) did NOT INCLUDE source map in release

I thinking should I open an @justinribeiro/lite-youtube Issue to suggest

  • include TS file
  • (OR exclude source map, but seem source map is helpful in some case)

馃檹 thanks @rschristian and @developit answering 馃檹

Generally a sourcemap should either include the sources content (the .ts file is inlined into the .map file as a string), or it links to the file (which definitely means that file should be included when publishing).

In general it's better to inline sources into the sourcemap files, since it's easier for bundlers to create derivative sourcemaps that way.

Btw - one other option you could consider would be to dynamically import this module. Dynamic imports are not executed during Preact CLI's prerendering.

Thanks for your reply !!

Was this page helpful?
0 / 5 - 0 ratings