├── .gitignore ├── .jpmignore ├── README.md ├── bin └── replace-html.js ├── index.js ├── install └── index.html ├── karma.conf.js ├── package.json ├── src ├── constants.js ├── content-script.js ├── main-sidebar.js ├── main.js ├── static │ ├── icon.svg │ ├── main.css │ └── sidebar.html └── utils │ └── get-metadata.js ├── test └── test-index.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | data/ 3 | node_modules/ 4 | .DS_Store 5 | *.xpi 6 | *.log 7 | -------------------------------------------------------------------------------- /.jpmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src/ 3 | *.xpi 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #ffmetadata 2 | 3 | A test to use webpack in add-on code 4 | 5 | # Note 6 | 7 | As of Firefox 48.0, `jpm run` no longer works on stable, so you will need to download a copy of Firefox Nightly. 8 | 9 | ## Usage 10 | 11 | First, `npm install`. Then you must run two commands in separate processes: 12 | 13 | - `npm start`, which runs all the watch/build processes for js, css, and dev server, 14 | - `npm run addon`, which runs the addon 15 | 16 | **You will need to open Firefox devtools and look for the "Metadata" panel** 17 | -------------------------------------------------------------------------------- /bin/replace-html.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | const htmlPath = path.resolve(__dirname, "../data/sidebar.html"); 5 | const htmlText = fs.readFileSync(htmlPath, "utf8"); 6 | 7 | fs.writeFileSync(htmlPath, htmlText.replace(/http:\/\/localhost:1936\//g, ""), "utf8"); 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Note: lib/main is a generated file. 2 | require("lib/main.js"); -------------------------------------------------------------------------------- /install/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 68 | 69 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '', 4 | frameworks: ['mocha'], 5 | files: [ 6 | 'test/**/*.js' 7 | ], 8 | reporters: ['progress'], 9 | port: 9876, 10 | colors: true, 11 | logLevel: config.LOG_DEBUG, 12 | autoWatch: true, 13 | browsers: ['Chrome'], 14 | singleRun: false 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "ffmetadata", 3 | "name": "ffmetadata", 4 | "version": "0.0.3", 5 | "description": "A basic add-on", 6 | "main": "index.js", 7 | "author": "", 8 | "engines": { 9 | "firefox": ">=38.0a1", 10 | "fennec": ">=38.0a1" 11 | }, 12 | "license": "MIT", 13 | "keywords": [ 14 | "jetpack" 15 | ], 16 | "directories": { 17 | "test": "test" 18 | }, 19 | "scripts": { 20 | "build": "webpack && cpx src/static/**/* data && node bin/replace-html", 21 | "start:js": "webpack -w", 22 | "start:static": "cpx src/static/**/* data -w", 23 | "start:server": "live-server data --port=1936 --no-browser", 24 | "start": "npm-run-all --parallel start:*", 25 | "addon": "jpm run -b nightly", 26 | "sign": "npm run build && jpm sign --api-key ${AMO_API_KEY} --api-secret ${AMO_API_SECRET}" 27 | }, 28 | "devDependencies": { 29 | "babel-core": "^6.9.1", 30 | "babel-loader": "^6.2.4", 31 | "babel-preset-react": "^6.5.0", 32 | "cpx": "^1.3.1", 33 | "jpm": "^1.0.7", 34 | "json-loader": "^0.5.4", 35 | "karma": "^0.13.22", 36 | "live-server": "^1.0.0", 37 | "mocha": "^2.4.5", 38 | "npm-run-all": "^2.3.0", 39 | "react": "^15.1.0", 40 | "react-dom": "^15.1.0", 41 | "webpack": "^1.13.0", 42 | "webpack-notifier": "^1.3.0" 43 | }, 44 | "dependencies": { 45 | "jsdom": "^9.0.0", 46 | "page-metadata-parser": "^0.2.0", 47 | "url": "^0.11.0", 48 | "url-parse": "^1.1.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | CONTENT_TO_ADDON_EVENT: "content_to_addon", 3 | ADDON_TO_CONTENT_EVENT: "addon_to_content" 4 | }; 5 | -------------------------------------------------------------------------------- /src/content-script.js: -------------------------------------------------------------------------------- 1 | 2 | function getData() { 3 | return { 4 | baseURI: document.baseURI, 5 | documentURI: document.documentURI, 6 | fullText: document.documentElement.innerHTML 7 | }; 8 | } 9 | 10 | self.port.on("message", e => { 11 | console.log("content script received message"); 12 | console.log(e); 13 | }); 14 | 15 | self.port.emit("message", {type: "PAGE_TEXT", data: getData()}); 16 | 17 | console.log("content-script loaded"); 18 | -------------------------------------------------------------------------------- /src/main-sidebar.js: -------------------------------------------------------------------------------- 1 | const {CONTENT_TO_ADDON_EVENT} = require("./constants"); 2 | const React = require("react"); 3 | const ReactDOM = require("react-dom"); 4 | 5 | const FormGroup = React.createClass({ 6 | render() { 7 | const hasValue = this.props.value !== null && typeof this.props.value !== "undefined"; 8 | return (
9 | 10 | {this.props.image && this.props.value && } 11 | {hasValue ? this.props.value : "(Not found)"} 12 |
); 13 | } 14 | }); 15 | 16 | const Main = React.createClass({ 17 | getInitialState() { 18 | return { 19 | metadata: {} 20 | }; 21 | }, 22 | componentDidMount() { 23 | const receive = event => { 24 | console.log("panel received event"); 25 | this.setState({metadata: {}}); 26 | this.setState({metadata: event.data}); 27 | }; 28 | window.addEventListener("message", function(event) { 29 | console.log("received port event"); 30 | window.port = event.ports[0]; 31 | window.port.onmessage = receive; 32 | }); 33 | }, 34 | onClick() { 35 | window.port.postMessage("REFRESH"); 36 | }, 37 | render() { 38 | const {metadata} = this.state; 39 | return (
40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 |
48 |
49 | 50 |
51 |
52 | 53 |
54 |
55 |
56 | 57 |
58 |           {JSON.stringify(metadata.metaTags, null, 2)}
59 |         
60 |
61 |
); 62 | } 63 | }); 64 | 65 | ReactDOM.render(
, document.getElementById("content")); 66 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const data = platform_require("sdk/self").data; 2 | const {CONTENT_TO_ADDON_EVENT, ADDON_TO_CONTENT_EVENT} = require("./constants"); 3 | const getMetadata = require("./utils/get-metadata"); 4 | const {Sidebar} = platform_require("sdk/ui/sidebar"); 5 | const {Panel} = platform_require("dev/panel"); 6 | const {Tool} = platform_require("dev/toolbox"); 7 | const {Class} = platform_require("sdk/core/heritage"); 8 | const tabs = platform_require("sdk/tabs"); 9 | const {MessageChannel} = platform_require("sdk/messaging"); 10 | 11 | const MetadataDebugger = Class({ 12 | extends: Panel, 13 | label: "Metadata", 14 | tooltip: "Metadata debugger", 15 | icon: data.url("icon.svg"), 16 | url: data.url("sidebar.html"), 17 | setup: function(options) { 18 | console.log("setup"); 19 | }, 20 | dispose: function() { 21 | console.log("dispose"); 22 | if (this.unload) this.unload(); 23 | this.unload = null; 24 | }, 25 | onReady: function() { 26 | console.log("ready"); 27 | const channel = new MessageChannel(); 28 | const addonSide = channel.port1; 29 | const panelSide = channel.port2; 30 | let tabWorker; 31 | let tab; 32 | 33 | function onContentScriptMessage(action) { 34 | if (action.type === "PAGE_TEXT") { 35 | const metadata = getMetadata(action.data); 36 | addonSide.postMessage(metadata); 37 | } 38 | } 39 | 40 | function onPanelMessage(e) { 41 | console.log(e); 42 | } 43 | 44 | function setWorker() { 45 | tab = tabs.activeTab; 46 | tabWorker = tab.attach({contentScriptFile: "content-script.js"}); 47 | tabWorker.port.on("message", onContentScriptMessage); 48 | tab.on("ready", setWorker); 49 | } 50 | 51 | setWorker(); 52 | addonSide.onmessage = e => tabWorker.port.emit("message", e.data); 53 | panelSide.onmessage = onPanelMessage; 54 | this.postMessage("port", [panelSide]); 55 | this.unload = () => { 56 | tab.off("ready", setWorker); 57 | worker.port.off("message", onContentScriptMessage); 58 | }; 59 | } 60 | }); 61 | 62 | const metadataTool = new Tool({ 63 | panels: {metadata: MetadataDebugger} 64 | }); 65 | -------------------------------------------------------------------------------- /src/static/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/static/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | 5 | *, 6 | *:before, 7 | *:after { 8 | box-sizing: inherit; 9 | } 10 | 11 | body { 12 | font-family: -apple-system, BlinkMacSystemFont, sans-serif; 13 | font-size: 13px; 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | [hidden] { 19 | display: none !important; 20 | } 21 | 22 | label { 23 | font-weight: bold; 24 | display: block; 25 | margin-bottom: 5px; 26 | } 27 | 28 | img { 29 | display: block; 30 | max-width: 200px; 31 | border: 1px solid #CCC; 32 | margin-bottom: 5px; 33 | } 34 | 35 | @media (min-width: 700px) { 36 | .container { 37 | display: flex; 38 | } 39 | .column { 40 | border-right: 1px solid #DDD; 41 | min-height: 100vh; 42 | } 43 | .column:first-child { 44 | flex-grow: 1; 45 | } 46 | .column:last-child { 47 | border-right: 0; 48 | } 49 | .form-group:last-child { 50 | border-bottom: 0; 51 | } 52 | .metatags { 53 | border-top: 1px solid #DDD; 54 | } 55 | } 56 | 57 | .form-group { 58 | padding: 10px; 59 | border-bottom: 1px solid #DDD; 60 | flex-direction: row; 61 | } 62 | .form-group.empty { 63 | color: #CCC; 64 | } 65 | 66 | pre { 67 | width: 100%; 68 | overflow-x: hidden; 69 | } 70 | 71 | .metatags { 72 | padding: 10px; 73 | } 74 | -------------------------------------------------------------------------------- /src/static/sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sidebar 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/utils/get-metadata.js: -------------------------------------------------------------------------------- 1 | const {Cc, Ci} = platform_require("chrome"); 2 | const metadataparser = require("page-metadata-parser"); 3 | const {resolve} = require("url"); 4 | 5 | // const metadataparser = new MetadataParser({ 6 | // site_name: [ 7 | // ['meta[property="og:site_name"]', node => node.element.content], 8 | // ] 9 | // }); 10 | 11 | function getDocumentObject(data) { 12 | const parser = Cc["@mozilla.org/xmlextras/domparser;1"] 13 | .createInstance(Ci.nsIDOMParser); 14 | // parser.init(null, data.documentURI, data.baseURI); 15 | return parser.parseFromString(data.fullText, "text/html"); 16 | } 17 | 18 | function tempFixUrls(data, baseUrl) { 19 | function resolveUrl(url) { 20 | if (!url) return url; 21 | url = url.replace(/\/\/^/, "http://"); 22 | return resolve(baseUrl, url); 23 | } 24 | data.image_url = resolveUrl(data.image_url); 25 | data.icon_url = resolveUrl(data.icon_url); 26 | data.url = resolveUrl(data.url || baseUrl); 27 | return data; 28 | } 29 | 30 | module.exports = function (data) { 31 | const htmlDoc = getDocumentObject(data); 32 | const result = tempFixUrls(metadataparser.getMetadata(htmlDoc), data.documentURI); 33 | result.metaTags = Array.map.call(null, htmlDoc.querySelectorAll("meta"), item => { 34 | const attributes = {}; 35 | Array.forEach.call(null, item.attributes, attr => attributes[attr.nodeName] = attr.nodeValue); 36 | return attributes; 37 | }); 38 | console.log(result.metaTags); 39 | return result; 40 | }; 41 | -------------------------------------------------------------------------------- /test/test-index.js: -------------------------------------------------------------------------------- 1 | describe("hello world", () => { 2 | it("should be ok", () => { 3 | console.log("hi"); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const WebpackNotifierPlugin = require("webpack-notifier"); 3 | const webpack = require("webpack"); 4 | const BannerPlugin = webpack.BannerPlugin; 5 | const path = require("path"); 6 | const absolute = (relPath) => path.join(__dirname, relPath); 7 | class AddonPlugin extends BannerPlugin { 8 | constructor(addonMainPath) { 9 | super("const platform_require = require; const platform_exports = exports;\n", { 10 | raw: true, 11 | include: addonMainPath 12 | }); 13 | } 14 | } 15 | 16 | let env = process.env.NODE_ENV || "development"; 17 | 18 | module.exports = { 19 | entry: { 20 | "/lib/main": "./src/main.js", 21 | "/data/content-script": "./src/content-script.js", 22 | "/data/main-sidebar": "./src/main-sidebar.js" 23 | }, 24 | output: { 25 | path: __dirname, 26 | filename: "[name].js", 27 | }, 28 | module: { 29 | loaders: [ 30 | {test: /\.json$/, loader: "json"}, 31 | { 32 | test: /.js?$/, 33 | loader: 'babel-loader', 34 | include: /src/, 35 | query: {presets: ['react']} 36 | } 37 | ] 38 | }, 39 | // devtool: env === "production" ? null : "eval", // This is for Firefox 40 | plugins: [ 41 | new WebpackNotifierPlugin(), 42 | new AddonPlugin("/lib/main") 43 | ] 44 | }; 45 | --------------------------------------------------------------------------------