cornerstonejs / cornerstoneWADOImageLoader

[DEPRECATED] DICOM WADO Image Loader for the cornerstone library
MIT License
281 stars 267 forks source link

Automatic publicPath is not supported in this browser #441

Open jamesdigid opened 2 years ago

jamesdigid commented 2 years ago

I'm getting the follow issues during our pipeline build after doing an upgrade.

Automatic publicPath is not supported in this browser
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';

Here is my webpack config:

// https://developers.google.com/web/tools/workbox/guides/codelabs/webpack
// ~~ WebPack
const path = require('path');
const merge = require('webpack-merge');
const webpack = require('webpack');
const webpackBase = require('./../../../.webpack/webpack.base.js');
// ~~ Plugins
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
  .BundleAnalyzerPlugin;
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExtractCssChunksPlugin = require('extract-css-chunks-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { InjectManifest } = require('workbox-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const GoogleFontsPlugin = require('@beyonk/google-fonts-webpack-plugin');
const ZipPlugin = require('zip-webpack-plugin');
// ~~ Rules
const extractStyleChunksRule = require('./rules/extractStyleChunks.js');
const replaceEnvVars = require('./replaceEnvVars.js');
// ~~ Build Type
const isProdBuild = process.env.NODE_ENV === 'production';
const isOnlineBuild = process.env.SS_BUILD_TYPE !== 'offline';
// ~~ Directories
const SRC_DIR = path.join(__dirname, '../src');
const PUBLIC_DIR = path.join(__dirname, '../public');
const CompressionPlugin = require('compression-webpack-plugin');
const DIST_DIR = path.join(__dirname, isOnlineBuild ? '../dist' : '../offline');
// ~~ Env Vars
const HTML_TEMPLATE = process.env.HTML_TEMPLATE || 'index.html';
const PUBLIC_URL = isOnlineBuild ? process.env.SS_APP_PUBLIC_URL || '/' : '';
const APP_CONFIG = process.env.APP_CONFIG || 'config/default.js';
const PROXY_TARGET = process.env.PROXY_TARGET;
const PROXY_DOMAIN = process.env.PROXY_DOMAIN;
const HOST_NAME = process.env.SS_APP_HOST_NAME || 'http://localhost';
const HOST_PORT = process.env.SS_APP_ROOT_PORT || 3000;

let SERVER_HOST = `${HOST_NAME}:${HOST_PORT}${PUBLIC_URL}`;
SERVER_HOST = SERVER_HOST.replace(/\/$/, ''); // ensure it does not end with /

const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;

const onProxyReqCors = (proxyReq, req, res) => {
  const method = req.method.toLowerCase();
  if (method === 'options') {
    // By-pass browser pre-flight options
    // Chrome sends `OPTIONS` header before making an actual request for
    // certain CORS requests (e.g. running on a port differnt than 80/443)
    // intercepting the traffic and sending an OK status allows the browser
    // to not panic about the OPTION request and send the real request.
    res.writeHead(200, req.headers);
    res.write(
      'Option Request was Successful!' +
        '\n' +
        JSON.stringify(req.headers, true, 2)
    );

    res.end();
  } else {
    proxyReq.headers = proxyReq.headers || {};
    proxyReq.headers['Access-Control-Allow-Origin'] = '*';
    proxyReq.headers['mode'] = 'no-cors';
  }
};

// @TODO Use onProxyReqCors
const setHeaders = (res, path) => {
  res.setHeader('Content-Type', 'text/plain');
  if (path.indexOf('.gz') !== -1) {
    res.setHeader('Content-Encoding', 'gzip');
  } else if (path.indexOf('.br') !== -1) {
    res.setHeader('Content-Encoding', 'br');
  }
};

module.exports = (env, argv) => {
  const baseConfig = webpackBase(env, argv, { SRC_DIR, DIST_DIR });
  const hasProxy = PROXY_TARGET && PROXY_DOMAIN;

  const mergedConfig = merge(baseConfig, {
    entry: {
      app: ENTRY_TARGET,
    },
    output: {
      path: DIST_DIR,
      filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js',
      publicPath: PUBLIC_URL, // Used by HtmlWebPackPlugin for asset prefix
      devtoolModuleFilenameTemplate: function(info) {
        if (isProdBuild) {
          return `webpack:///${info.resourcePath}`;
        } else {
          return 'file:///' + encodeURI(info.absoluteResourcePath);
        }
      },
    },
    resolve: {
      // We use this alias and the CopyPlugin below to support using the dynamic-import version
      // of WADO Image Loader, but only when building a PWA. When we build a package, we must use the
      // bundled version of WADO Image Loader so we can produce a single file for the viewer.
      // (Note: script-tag version of the viewer will no longer be supported in OHIF v3)
      alias: {
        // 'cornerstone-wado-image-loader':
        //   'cornerstone-wado-image-loader/dist/dynamic-import/cornerstoneWADOImageLoader.min.js',
        'cornerstone-wado-image-loader':
          'cornerstone-wado-image-loader/dist/cornerstoneWADOImageLoader.min.js',
      },
    },
    module: {
      rules: [...extractStyleChunksRule(isProdBuild)],
    },
    plugins: [
      // Uncomment to generate bundle analyzer
      // new BundleAnalyzerPlugin(),
      // Clean output.path
      new CleanWebpackPlugin(),
      // Copy "Public" Folder to Dist
      new CompressionPlugin(),
      new CopyWebpackPlugin([
        {
          from: PUBLIC_DIR,
          to: DIST_DIR,
          toType: 'dir',
          // Ignore our HtmlWebpackPlugin template file
          // Ignore our configuration files
          ignore: ['config/*', 'html-templates/*', '.DS_Store'],
        },
        // Short term solution to make sure GCloud config is available in output
        // for our docker implementation
        {
          from: `${PUBLIC_DIR}/config/google.js`,
          to: `${DIST_DIR}/google.js`,
        },
        {
          from: `${PUBLIC_DIR}/.htaccess`,
          to: `${DIST_DIR}/`,
        },
        // Copy over and rename our target app config file
        {
          from: `${PUBLIC_DIR}/${APP_CONFIG}`,
          to: `${DIST_DIR}/app-config.js`,
          transform(content) {
            return replaceEnvVars(content, process.env);
          },
        },
        {
          from:
            '../../../node_modules/cornerstone-wado-image-loader/dist/dynamic-import',
          to: DIST_DIR,
        },
      ]),
      // https://github.com/faceyspacey/extract-css-chunks-webpack-plugin#webpack-4-standalone-installation
      new ExtractCssChunksPlugin({
        filename:
          isProdBuild && isOnlineBuild ? '[name].[hash].css' : '[name].css',
        chunkFilename:
          isProdBuild && isOnlineBuild ? '[id].[hash].css' : '[id].css',
        ignoreOrder: false, // Enable to remove warnings about conflicting order
      }),
      // Generate "index.html" w/ correct includes/imports
      new HtmlWebpackPlugin({
        template: `${PUBLIC_DIR}/html-templates/${HTML_TEMPLATE}`,
        filename: 'index.html',
        templateParameters: {
          PUBLIC_URL: PUBLIC_URL,
        },
      }),
      // No longer maintained; but good for generating icons + manifest
      // new FaviconsWebpackPlugin( path.join(PUBLIC_DIR, 'assets', 'icons-512.png')),
      new InjectManifest({
        swDest: 'sw.js',
        swSrc: path.join(SRC_DIR, 'service-worker.js'),
        // Increase the limit to 4mb:
        // maximumFileSizeToCacheInBytes: 4 * 1024 * 1024
      }),
      // Bundle with Google Web Fonts
      new GoogleFontsPlugin({
        //apiUrl: 'https://n8n-google-fonts-helper.herokuapp.com/api/fonts',
        fonts: [
          { family: 'Roboto', variants: ['100', '300', '400', '500', '700'] },
        ],
      }),
    ],
    optimization: {
      splitChunks: {
        // include all types of chunks
        chunks: 'all',
      },
      //runtimeChunk: 'single',
      minimize: isProdBuild,
      sideEffects: true,
    },
    // https://webpack.js.org/configuration/dev-server/
    devServer: {
      // gzip compression of everything served
      // Causes Cypress: `wait-on` issue in CI
      // compress: true,
      // http2: true,
      // https: true,
      /*
      before(app) {
        app.use((req, res, next) => {
          res.header('Cross-Origin-Opener-Policy', 'same-origin');
          res.header('Cross-Origin-Embedder-Policy', 'require-corp');
          next();
        });
      },*/
      hot: true,
      //open: true,
      port: HOST_PORT,
      host: 'localhost',
      open: ['/imageviewer/'],
      client: {
        overlay: { errors: true, warnings: false },
      },
      static: [
        {
          directory: path.join(require('os').homedir(), 'dicomweb'),
          staticOptions: {
            extensions: ['gz', 'br'],
            index: 'index.json.gz',
            redirect: true,
            setHeaders,
          },
          publicPath: '/dicomweb',
        },
        {
          directory: '../../testdata',
          staticOptions: {
            extensions: ['gz', 'br'],
            index: 'index.json.gz',
            redirect: true,
            setHeaders,
          },
          publicPath: '/testdata',
        },
      ],
      //publicPath: 'https://localhost:3000/imageviewer/',
      //writeToDisk: true,
      historyApiFallback: {
        disableDotRule: true,
        index: PUBLIC_URL + 'index.html',
      },
      headers: {
        'Cross-Origin-Embedder-Policy': 'require-corp',
        'Cross-Origin-Opener-Policy': 'same-origin',
      },
      proxy: {
        '/wado': {
          target: `https://${PROXY_DOMAIN}/orthanc`,
          changeOrigin: true,
          secure: false,
          onProxyReq: onProxyReqCors,
        },
        '/dicom-web': {
          target: `https://${PROXY_DOMAIN}/orthanc`,
          changeOrigin: true,
          secure: false,
          onProxyReq: onProxyReqCors,
        },
        '/api': {
          target: `https://${PROXY_DOMAIN}`,
          changeOrigin: true,
          secure: true,
          onProxyReq: onProxyReqCors,
        },

        '/broadcasting': {
          target: `https://${PROXY_DOMAIN}`,
          changeOrigin: true,
          secure: true,
          onProxyReq: onProxyReqCors,
        },
      },
    },
    devtool: 'inline-source-map',
  });

  if (hasProxy) {
    mergedConfig.devServer.proxy = {};
    mergedConfig.devServer.proxy[PROXY_TARGET] = PROXY_DOMAIN;
  }

  if (isProdBuild) {
    mergedConfig.optimization.minimizer.push(new OptimizeCSSAssetsPlugin({}));
  }

  if (isProdBuild && !isOnlineBuild) {
    mergedConfig.plugins.push(
      new ZipPlugin({
        path: '../dist',
        filename: 'ivis.zip',
        pathPrefix: 'IVIS',
      })
    );
  }

  if (!isProdBuild) {
    mergedConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
  }

  return mergedConfig;
};

Any suggestions or advice would be appreciated. Thanks

naveenraina commented 2 years ago

I am also facing this issue in latest version.

Zaid-Safadi commented 2 years ago

I created a project with min. configuration to reproduce this. I believe this is an issue with jest not supporting the webpack "auto" configuration https://github.com/Zaid-Safadi/wado-loader-unit-test/tree/master

@swederik , this started happening after setting the publicPath to auto in this commit #399 , is this needed for production to make the library work?

swederik commented 2 years ago

I believe it was necessary when I was getting the dynamic imports working for the codecs inside the web workers. Previously we had an empty string, then there was a commit which set it to '/' (https://github.com/cornerstonejs/cornerstoneWADOImageLoader/commit/f88b236dc9017db3956611309398c6b9609112c2) and then 'auto', which also looks like it is the default for the option itself: https://webpack.js.org/configuration/output/#outputpublicpath

If you don't want it to be set to auto, what do you want it to be set to? "/" ? Won't that break when someone tries to use a consuming application (e.g. OHIF) at a subdirectory (e.g. mywebsite.com/ohif/)? It will try to fetch the codecs from mywebsite.com/ and fail. Open to ideas!

Zaid-Safadi commented 2 years ago

I think setting the value in the library to "auto" is the right thing to allow the consuming app to configure this based on the hosting url. It just doesn't seem to be supported with jest and causing any unit test that reference the library to fail.

Cypress seems to have a similar issue but there are some workarounds here: https://github.com/cypress-io/cypress/issues/18435

Trying to see if there is a similar solution or workaround for jest otherwise no unit tests can run with the library

desprit commented 2 years ago

@swederik

cornerstoneWebImageLoader doesn't set it to auto: https://github.com/cornerstonejs/cornerstoneWebImageLoader/blob/master/config/webpack/webpack-base.js

cornerstoneTools doesn't set it too: https://github.com/cornerstonejs/cornerstoneTools/blob/master/config/webpack/webpack-base.js

It's currently failing for me with Jest, Cypress and Vitest.

Zaid-Safadi commented 2 years ago

I don't think this is something caused by an update to cornerstoneWADOImageLoader but more to the updated versions of the webpack/jest...

The way I solved this is by using the library https://github.com/jsdom/jsdom as a dev dependency to provide the required DOM objects

What you will need is on startup/init of your unit-test library (e.g. jest)

  1. create the document and window objects and assign them to global
  2. Define a "currentScript" property on your document object and set the src value to '/'
  3. Define TextEncoder/TextDecoder properties on the window object as these are missing from JSDOM: https://github.com/jsdom/jsdom/issues/2524
  4. For JEST, In your unit test file, use jsdom jest environment / @jest-environment jsdom */
desprit commented 2 years ago

@Zaid-Safadi

Thank you, I was able to do that in "vitest" in "jsdom" environment with the following:

const script = global.document.createElement("script");
script.setAttribute("src", "/");
Object.defineProperty(global.document, "currentScript", {
  value: script
});

update: same for the Cypress

kmcgurty commented 1 year ago

For me, I'm using SvelteKit + this library. I found that webpack was looking for document.currentScript (as stated above), but whatever reason that doesn't exist when using SvelteKit. In the webpack code, if that fails it looks for the last <script> tag on the page, and looks for its src attribute. In the instance of SvelteKit, that also doesn't exist, so it throws the error in the OP.

If you used vite to scaffold your project, your app.html should look something like this.

The workaround I've found is to simply change app.html to have a script tag at the end of the file like this:

<!DOCTYPE html>
<html lang="en">
    <head>
        <!-- head stuff -->
    </head>

    <body data-sveltekit-preload-data="hover">
        <div style="display: contents">%sveltekit.body%</div>
    </body>

    <!-- workaround to let webpack scripts work properly -->
    <script type="text/javascript" src=""></script>
</html>

Hope this helps someone else. I spent a good week looking into this, and finally figured out the cause.

Amnesthesia commented 4 months ago

I don't think this is something caused by an update to cornerstoneWADOImageLoader but more to the updated versions of the webpack/jest...

We're getting this in Storybook after adding cornerstone-wado-image-loader. Removing this dependency makes the error go away.