├── .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 |
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 |
--------------------------------------------------------------------------------