Skip to content

Commit

Permalink
Port webpack-target-webextension to Rspack
Browse files Browse the repository at this point in the history
  • Loading branch information
cezaraugusto committed Aug 23, 2024
1 parent 9b2e141 commit 9132320
Show file tree
Hide file tree
Showing 15 changed files with 862 additions and 963 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs'
import path from 'path'
import {type Compiler} from '@rspack/core'
import WebExtension from 'webpack-target-webextension'
import {WebExtensionPlugin} from './rspack-target-webextension'
import {type PluginInterface} from '../../../reload-types'
import {type Manifest} from '../../../../webpack-types'
import {type DevOptions} from '../../../../../commands/dev'
Expand Down Expand Up @@ -139,7 +139,7 @@ class TargetWebExtensionPlugin {

this.handleBackground(compiler, this.browser, manifest)

new WebExtension({
new WebExtensionPlugin({
background: this.getEntryName(manifest),
weakRuntimeCheck: true
}).apply(compiler)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @ts-check
const basic = [
`var isBrowser = !!(() => { try { return browser.runtime.getURL("/") } catch(e) {} })()`,
`var isChrome = !!(() => { try { return chrome.runtime.getURL("/") } catch(e) {} })()`
]
const strong = [
...basic,
`var runtime = isBrowser ? browser : isChrome ? chrome : { get runtime() { throw new Error("No chrome or browser runtime found") } }`
]
const weak = [
...basic,
`var runtime = isBrowser ? browser : isChrome ? chrome : (typeof self === 'object' && self.addEventListener) ? { get runtime() { throw new Error("No chrome or browser runtime found") } } : { runtime: { getURL: x => x } }`
]

export default (getWeak = false) => (getWeak ? weak : strong)
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {Compiler} from '@rspack/core'

import {LoadScriptRuntimeModule} from './RuntimeModules/LoadScript'
import {PublicPathRuntimeModule} from './RuntimeModules/PublicPath'
import {AutoPublicPathRuntimeModule} from './RuntimeModules/AutoPublicPath'
import {ChunkLoaderFallbackRuntimeModule} from './RuntimeModules/ChunkLoaderFallback'
import {BackgroundOptions} from './types'

export class ChuckLoaderRuntimePlugin {
private readonly options: BackgroundOptions
private readonly weakRuntimeCheck: boolean

constructor(options: BackgroundOptions, weakRuntimeCheck: boolean) {
this.options = options
this.weakRuntimeCheck = weakRuntimeCheck
}

apply(compiler: Compiler) {
const {RuntimeGlobals} = compiler.webpack
const {options} = this

compiler.hooks.compilation.tap(
ChuckLoaderRuntimePlugin.name,
(compilation) => {
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.loadScript)
.tap(ChuckLoaderRuntimePlugin.name, (chunk) => {
compilation.addRuntimeModule(
chunk,
LoadScriptRuntimeModule(
compiler.webpack,
compilation.outputOptions.environment &&
compilation.outputOptions.environment.dynamicImport,
options && options.classicLoader !== false,
this.weakRuntimeCheck
)
)
return true
})

compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.publicPath)
.tap(ChuckLoaderRuntimePlugin.name, (chunk, set) => {
const {outputOptions} = compilation
const {publicPath, scriptType} = outputOptions

if (publicPath === 'auto') {
const module = AutoPublicPathRuntimeModule(
compiler.webpack,
this.weakRuntimeCheck
)

if (scriptType !== 'module') {
set.add(RuntimeGlobals.global)
}

compilation.addRuntimeModule(chunk, module)
} else {
const module = PublicPathRuntimeModule(
compiler.webpack,
this.weakRuntimeCheck
)

if (
typeof publicPath !== 'string' ||
/\[(full)?hash\]/.test(publicPath)
) {
module.fullHash = true
}

compilation.addRuntimeModule(chunk, module)
}
return true
})

if (options && options.classicLoader !== false) {
compilation.hooks.afterChunks.tap(
ChuckLoaderRuntimePlugin.name,
() => {
const {entry, pageEntry, serviceWorkerEntry} = options
const entryPoint = entry && compilation.entrypoints.get(entry)
const entryPoint2 =
pageEntry && compilation.entrypoints.get(pageEntry)
const entryPoint3 =
serviceWorkerEntry &&
compilation.entrypoints.get(serviceWorkerEntry)

for (const entry of [entryPoint, entryPoint2, entryPoint3]) {
if (!entry) continue

compilation.addRuntimeModule(
entry.chunks[0],
ChunkLoaderFallbackRuntimeModule(compiler.webpack)
)
}
}
)
}
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {Compiler} from '@rspack/core'

// @ts-check
export class HMRDevServerPlugin {
apply(compiler: Compiler) {
if (!compiler.options.devServer) compiler.options.devServer = {}
const devServer = compiler.options.devServer

setDefault(devServer, 'devMiddleware', {})
// Extensions cannot be loaded over network
setDefault(devServer.devMiddleware, 'writeToDisk', true)

if (!devServer.hot) return

setDefault(devServer, 'host', '127.0.0.1')
setDefault(devServer, 'client', {
overlay: false,
progress: false,
webSocketURL: {
protocol: 'ws'
}
})
// Overlay doesn't work well in content script.
setDefault(devServer.client, 'overlay', false)
// Progress is annoying in console.
setDefault(devServer.client, 'progress', false)
// In content script loaded in https:// pages, it will try to use wss:// because of protocol detect.
setDefault(devServer.client, 'webSocketURL', {protocol: 'ws'})
setDefault(devServer.client.webSocketURL, 'protocol', 'ws')

// HMR requires CORS requests in content scripts.
setDefault(devServer, 'allowedHosts', 'all')
setDefault(devServer, 'headers', {
'Access-Control-Allow-Origin': '*'
})

// Avoid listening to node_modules
setDefault(devServer, 'static', {watch: {ignored: /\bnode_modules\b/}})
setDefault(devServer.static, 'watch', {ignored: /\bnode_modules\b/})
isObject(devServer.static) &&
setDefault(devServer.static.watch, 'ignored', /\bnode_modules\b/)
}
}

function setDefault(obj: {[x: string]: any}, key: string | number, val: any) {
if (isObject(obj) && obj[key] === undefined) obj[key] = val
}

function isObject(x: any): x is Record<string, any> {
return typeof x === 'object' && x !== null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {Compiler} from '@rspack/core'

// @ts-check
export class NoDangerNamePlugin {
apply(compiler: Compiler) {
const Error = compiler.webpack.WebpackError

// Chrome bug https://bugs.chromium.org/p/chromium/issues/detail?id=1108199
{
const optimization = compiler.options.optimization

if (optimization.splitChunks === undefined)
optimization.splitChunks = {automaticNameDelimiter: '-'}
else if (
optimization.splitChunks &&
optimization.splitChunks.automaticNameDelimiter === undefined
) {
optimization.splitChunks.automaticNameDelimiter = '-'
}
}

compiler.hooks.emit.tap(NoDangerNamePlugin.name, (compilation) => {
const with_ = []
const withTilde = []
for (const file in compilation.assets) {
if (file.startsWith('_')) {
if (file.startsWith('_locales/') || file === '_locales') {
} else with_.push(String(file))
}
if (file.includes('~')) withTilde.push(String(file))
}
if (with_.length) {
compilation.errors.push(
new Error(
`[webpack-extension-target]
Path starts with "_" is preserved by the browser.
The browser will refuse to load this extension.
Please adjust your webpack configuration to remove that.
File(s) starts with "_":
` + with_.map((x) => ' ' + x).join('\n')
)
)
}
if (withTilde.length) {
compilation.errors.push(
new Error(
`[webpack-extension-target]
File includes "~" is not be able to loaded by Chrome due to a bug https://bugs.chromium.org/p/chromium/issues/detail?id=1108199.
Please adjust your webpack configuration to remove that.
If you're using splitChunks, please set config.optimization.splitChunks.automaticNameDelimiter to other char like "-".
If you're using runtimeChunks, please set config.optimization.runtimeChunk.name to a function like
entrypoint => \`runtime-\${entrypoint.name}\`
File(s) includes "~":
` + withTilde.map((x) => ' ' + x).join('\n')
)
)
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {type rspack} from '@rspack/core'
import BrowserRuntime from '../BrowserRuntime'

/**
* @returns {import('webpack').RuntimeModule}
*/
export function AutoPublicPathRuntimeModule(
webpack: typeof rspack,
acceptWeak: boolean
) {
const {
RuntimeModule,
RuntimeGlobals,
Template,
javascript: {JavascriptModulesPlugin}
} = webpack

class AutoPublicPathRuntimeModule extends RuntimeModule {
constructor() {
super('publicPath', RuntimeModule.STAGE_BASIC)
}

/**
* @returns {string} runtime code
*/
generate() {
const {compilation} = this

if (!compilation)
return Template.asString(
'/* [webpack-target-webextension] AutoPublicPathRuntimeModule skipped because no compilation is found. */'
)
const {scriptType, importMetaName} = compilation.outputOptions
const chunkName = compilation.getPath(
JavascriptModulesPlugin.getChunkFilenameTemplate(
this.chunk,
compilation.outputOptions
),
{
chunk: this.chunk,
contentHashType: 'javascript'
}
)
const outputPath = compilation.outputOptions.path
if (!outputPath)
return Template.asString(
'/* [webpack-target-webextension] AutoPublicPathRuntimeModule skipped because no output path is found. */'
)
const undoPath = getUndoPath(chunkName, outputPath, false)

return Template.asString([
...BrowserRuntime(acceptWeak),
'var scriptUrl;',
scriptType === 'module'
? `if (typeof ${importMetaName}.url === "string") scriptUrl = ${importMetaName}.url`
: Template.asString([
`if (${RuntimeGlobals.global}.importScripts) scriptUrl = ${RuntimeGlobals.global}.location + "";`,
`var document = ${RuntimeGlobals.global}.document;`,
'if (!scriptUrl && document && document.currentScript) {',
Template.indent(`scriptUrl = document.currentScript.src`),
'}'
]),
'// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration',
'// or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.',
'if (!scriptUrl) {',
Template.indent([
'if (isChrome || isBrowser) scriptUrl = runtime.runtime.getURL("/");',
'else throw new Error("Automatic publicPath is not supported in this browser");'
]),
'}',
'scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\\?.*$/, "").replace(/\\/[^\\/]+$/, "/");',
!undoPath
? `${RuntimeGlobals.publicPath} = scriptUrl;`
: `${RuntimeGlobals.publicPath} = scriptUrl + ${JSON.stringify(undoPath)};`
])
}
}

return new AutoPublicPathRuntimeModule()
}

/**
* The following function (from webpack/lib/util/identifier) is not exported by Webpack 5 as a public API.
* To not import anything from Webpack directly, this function is copied here.
*
* It follows the MIT license.
*
* @param {string} filename the filename which should be undone
* @param {string} outputPath the output path that is restored (only relevant when filename contains "..")
* @param {boolean} enforceRelative true returns ./ for empty paths
* @returns {string} repeated ../ to leave the directory of the provided filename to be back on output dir
*/
function getUndoPath(
filename: string,
outputPath: string,
enforceRelative: boolean
): string {
let depth = -1
let append = ''
outputPath = outputPath.replace(/[\\/]$/, '')
for (const part of filename.split(/[/\\]+/)) {
if (part === '..') {
if (depth > -1) {
depth--
} else {
const i = outputPath.lastIndexOf('/')
const j = outputPath.lastIndexOf('\\')
const pos = i < 0 ? j : j < 0 ? i : Math.max(i, j)
if (pos < 0) return outputPath + '/'
append = outputPath.slice(pos + 1) + '/' + append
outputPath = outputPath.slice(0, pos)
}
} else if (part !== '.') {
depth++
}
}
return depth > 0
? `${'../'.repeat(depth)}${append}`
: enforceRelative
? `./${append}`
: append
}
Loading

0 comments on commit 9132320

Please sign in to comment.