chibat / chrome-extension-typescript-starter

Chrome Extension TypeScript Starter
MIT License
2.39k stars 403 forks source link

Webpack issue using extra npm packages #52

Open DenisTimofijuk opened 1 year ago

DenisTimofijuk commented 1 year ago

I am using inject technology to be able to access window object from my content scripts as per this example: https://gist.github.com/devjin0617/3e8d72d94c1b9e69690717a219644c7a Everything was working fine until I added npm seedrandom to my inject file. Now when I build my project, inject file is modified in such a way that it no longer is working as it should. Tried various webpack settings, but with no luck. Should this be working by default using such a prebuild environment?

 "dependencies": {
    "seedrandom": "^3.0.5"
  },
  "devDependencies": {
    "@types/chrome": "0.0.158",
    "@types/jquery": "^3.5.14",
    "@types/seedrandom": "^3.0.4",
    "copy-webpack-plugin": "^9.0.1",
    "glob": "^7.1.6",
    "prettier": "^2.2.1",
    "rimraf": "^3.0.2 ",
    "ts-loader": "^8.0.0",
    "typescript": "^4.4.3 ",
    "webpack": "^5.0.0",
    "webpack-cli": "^4.0.0",
    "webpack-merge": "^5.0.0"
  }

module.exports = {
    entry: {
      popup: path.join(srcDir, 'popup/popup.ts'),
      options: path.join(srcDir, 'options/options.ts'),
      background: path.join(srcDir, 'background/background.ts'),
      content: path.join(srcDir, 'content/content.ts'),
      inject: path.join(srcDir, 'inject/inject.ts'),
    //   vendor_inject: ['seedrandom'],
    },
    output: {
        path: path.join(__dirname, "../dist/js"),
        filename: "[name].js",
    },
    optimization: {
        splitChunks: {
            // name: "vendor",
            // chunks(chunk) {
            //   return chunk.name !== 'background';
            // },
            cacheGroups: {
                vendor: {
                  name: 'vendor',
                  chunks(chunk) {
                      return chunk.name !== 'background';
                    },
                },
                vendor_inject: {
                  test: /[\\/]node_modules[\\/](seedrandom)[\\/]/,
                  name: 'vendor_inject',
                  chunks: 'all'
                }
              }
        },
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: "ts-loader",
                exclude: /node_modules/,
            },
        ],
    },
    resolve: {
        extensions: [".ts", ".tsx", ".js"],
    },
    plugins: [
        new CopyPlugin({
            patterns: [{ from: ".", to: "../", context: "public" }],
            options: {},
        }),
    ],
};
DenisTimofijuk commented 1 year ago

My progress with ChatGPT (no luck):

WEBPACK CONFIGURATION:

const webpack = require("webpack");
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const srcDir = path.join(__dirname, "..", "src");

module.exports = {
    entry: {
      popup: path.join(srcDir, 'popup/popup.ts'),
      options: path.join(srcDir, 'options/options.ts'),
      background: path.join(srcDir, 'background/background.ts'),
      content: path.join(srcDir, 'content/content.ts'),
      inject: path.join(srcDir, 'inject/inject.ts'),
    },
    output: {
        path: path.join(__dirname, "../dist/js"),
        filename: "[name].js",
    },
    optimization: {
        splitChunks: {
            name: "vendor",
            chunks(chunk) {
              return chunk.name !== 'background';
            },
        },
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: "ts-loader",
                exclude: /node_modules/,
            },
        ], 
    },
    resolve: {
        extensions: [".ts", ".tsx", ".js"],
    },
    plugins: [
        new CopyPlugin({
            patterns: [{ from: ".", to: "../", context: "public" }],
            options: {},
        }),
        new webpack.ProvidePlugin({
            seedrandom: 'seedrandom'
          })
    ],
};

NumericList.ts:


import Compositor from "../../Compositor";
import { handleIgnore } from "../../global BOT rules";
import { getAvailableAnswers, getSimpleText, randomIntFromInterval, warnUnhandled } from "../../global helpers";
import { getMin, getMax, getCurrentSum } from "./Getters";
import { collectSermoRangeValues, setNumericValuesBySermoRange, handleAnswersForTotalSumWithRanges } from "./rulesHandlers";
import { multiSumEequal, multiSumMax, multiSumMin } from "./sumGenerators";
const compositor = Compositor.getInstance();
const PROTOCOL_MAX_TRIES = 100;
const NUM_QUARTERS = 4;

// . . . 
// removed some code for simplicity 
// . . . 

export function shuffleQuarters(respondentId:number) {
  const quarterSize = PROTOCOL_MAX_TRIES / NUM_QUARTERS;
  debugger;
  // @ts-ignore
  const rng = seedrandom(respondentId.toString()); // Use the seedrandom library for consistent random numbers

  // Generate a shuffled list of quarter indices
  const quarterIndices = Array.from({length: NUM_QUARTERS}, (_, i) => i);
  //shuffleArray(quarterIndices, rng);

  // Generate a shuffled list of indices within each quarter
  const shuffledIndices = [];
  for (let i = 0; i < NUM_QUARTERS; i++) {
    const quarterStart = quarterIndices[i] * quarterSize;
    const indices = Array.from({length: quarterSize}, (_, j) => quarterStart + j);
    // shuffleArray(indices, rng);
    shuffledIndices.push(...indices);
  }

  return shuffledIndices;
}

// @ts-ignore
function shuffleArray(arr:Array<any>, rng:seedrandom.PRNG) {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
}

Inject.ts:


import { initMain } from "./local_content/init_main";
import { loadRules } from "./local_content/loadRules";
import "./local_content/collectValidationMessages";
import shuffleQuarters from "./handlers/NumericList/NumericList";

declare namespace chrome {
    let it_is_not_supported_here: null;
}

console.log('Preparing inject.js')

if (window.Confirmit && window.Confirmit.page) {
    loadRules();
    initMain();
    shuffleQuarters('myID');
    console.log('%c Inject.js ready. ', 'background: #00EAD7; color: #EA0013');
}

As you can see, inject.ts is self-invoking since it is an entry file. Once I use this code ### const rng = seedrandom(respondentId.toString()); ### in NumericList.ts file, after building with webpack inject.ts is no longer self-invoking. How to fix this?

DenisTimofijuk commented 1 year ago

Created a demonstration project: https://github.com/DenisTimofijuk/injectIssueExample Once I enabling rng = seedrandom('MyID'); in the inject.ts file, this file no longer self-invoking and that is the issue for me. How to solve it?

ale-ben commented 8 months ago

I might have some updates on this. First of all, this was my situation:

content_script.tsx:

import { sha256 } from "js-sha256";

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
    if (sender.tab === undefined && request.event && request.event === 'toggle')
        console.log(sha256("Hello world"));
    sendResponse({ farewell: 'goodbye' });
});

console.log('content_script.tsx loaded');

background.ts:

chrome.action.onClicked.addListener(async (tab) => {
    chrome.tabs
        .sendMessage(tab.id ? tab.id : -1, { event: 'toggle' })
        .catch((error) => {
            console.error(error);
        });
});

Content script has a console log as soon as it is loaded. When I added console.log(sha256("Hello world"));, the file stopped loading (no more console.log). Note that this only happened with npm installed packages, had no problem with import on my files.

I found 2 things:

1.

Moving the import and the console.log(sha....) from context script to background solved everything. This would work but imply sending a message to background every time I had to generate SHA of something.

2.

Seems like content script does not work well with standard import / export. Using dynamic import for npm packages seems to solve the issue (see this answer on Stackoverflow. IMPORTANT: If you follow this option, READ THE NOTE AT THE END OF THIS POST.

After using dynamic import, my content script looks like this:

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
    if (sender.tab === undefined && request.event && request.event === 'toggle')
        const sha256 = await import('js-sha256');
            console.log(sha256.sha256("Hello world"));
    sendResponse({ farewell: 'goodbye' });
});

console.log('content_script.tsx loaded');

And everything works as intended.

NOTE: Using dynamic imports resulted in the following error: Uncaught Error: Automatic publicPath is not supported in this browser

This can be easily solved by changing in webpack.common.js the output section from

output: {
        path: path.join(__dirname, "../dist/js"),
        filename: "[name].js"
    },

to

output: {
        path: path.join(__dirname, "../dist/js"),
        filename: "[name].js",
       publicPath: ''
    },

Also you have to update module in tsconfig.json from "module": "ES6", to "module": "ES2020",

UPDATE: dynamic import seems to lead to a policy violation: content_script.js:972 Refused to load the script 'http://192.168.1.22:5500/docs/vendor.js' because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:*". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback. . Still figuring out how to solve this

UPDATE 2: Apparently Content Security Policies are a pain and chrome does not allow to add other sources so it seems that the only options are either copy the source code in the src folder or use message passing

UPDATE 3: Got to the conclusion that Webopack works like shit in this context, migrated project a VITE implementation that offers realtime reload (and it works perfectly) and out of the box import support. You can find it here: https://crxjs.dev/vite-plugin https://github.com/crxjs/chrome-extension-tools