├── .gitignore ├── CHANGELOG.md ├── README.md ├── package.json ├── src ├── gear.png ├── icon.png ├── index.html ├── index.ts ├── manifest.json ├── readwiseStuff.ts ├── refresh.png ├── snooker.png └── style.css ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | @updates.md 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build folder 10 | dist 11 | .vscode 12 | 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v4 2 | - Bug fix: highlights are now exported in oldest to newest date order 3 | - Bug fix: Fixed "Last Highlight Date" 4 | - New: now exports in the date order of the highlights, and includes a header for the date, thus grouping highlights by date 5 | 6 | # v3 7 | - Image export -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Update from 2022-05-26 2 | Since Craft has slowed down or reoriented themselves with their API, I have decided for now to archive this plugin. It is still working, but I will continue development on it because of the lack of API progress. If someone is interested in taking over this repository, please let me know. 3 | 4 | 5 | 6 | # Introduction 7 | A Craft X extension to import [Readwise](https://readwise.io/) highlights into [Craft](https://www.craft.do/). 8 | 9 | [![Quick overview](https://github.com/TfTHacker/craft42-readwise/releases/download/resources/IntroVideo.gif)](https://www.loom.com/share/5c601a6fe43c4b06a57d9f27a5dfb945) 10 | 11 | # Installing the Extension into Craft 12 | 13 | [![Installation](https://github.com/TfTHacker/craft42-readwise/releases/download/resources/InstallVideo.gif)](https://www.loom.com/share/849a920a8cba4b199dd90a1718f8b025) 14 | 15 | ### Step by step 16 | To install this extension, first download it: 17 | 1. From this GitHub page, click on "Releases" 18 | 1. A list of releases is listed 19 | 1. Expand the first release at the top of the page and download the **craft42-readwise.craftx** file 20 | 21 | Now with the extension downloaded, let us add it to Craft. 22 | 23 | > Note: These instructions assumed you have enabled extension support in Craft. 24 | 25 | 1. Open Craft and click on the button to add an extension 26 | 1. Find the file downloaded in the previous step and select it 27 | 1. Confirm that you want to install the extension 28 | 1. The extension will now be available in the list of installed extensions. Click on the Readwise Highlights extension to open it. 29 | 30 | Now provide your private Readwise access token 31 | 1. From https://readwise.io/access_token, get your access token and copy it. 32 | 1. In the extension, copy the access token into the field provided for it and then click Go 33 | 34 | This extension is now configured for use 35 | 36 | # Features 37 | ## Insert Highlights from a Document 38 | The extension opens to listing the documents with highligths you have in Readwise. The most recently highlighted documents appear at the top. 39 | 40 | Next to each document is a small icon for importing the highlights of that document into craft. Click that icon and the highlights will from that document will be appended to the current file open in craft 41 | 42 | ## Random Highlight 43 | Along the bottom of the extension is a 8-ball button. This will grab a random highlight from your Readwise highlights and insert it into the document. 44 | 45 | ## Refresh button 46 | Along the bottom of the extension is a refresh button that refreshs the book list displayed in the plugin. 47 | 48 | ## Config Button 49 | Along the bottom of the extension is a button for configuring the extension. 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-extension", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "webpack --mode=production", 7 | "dev": "webpack serve --mode=development" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@craftdocs/craft-extension-api": "0.0.4", 13 | "@craftdocs/craft-extension-api-sdk": "^0.0.4", 14 | "copy-webpack-plugin": "^10.2.0", 15 | "css-loader": "^6.5.1", 16 | "html-webpack-plugin": "^5.5.0", 17 | "react-dev-utils": "^12.0.0", 18 | "style-loader": "^3.3.1", 19 | "ts-loader": "^9.2.6", 20 | "typescript": "^4.5.4", 21 | "url-loader": "^4.1.1", 22 | "webpack": "^5.64.5", 23 | "webpack-cli": "^4.9.1", 24 | "webpack-dev-server": "^4.6.0", 25 | "zip-webpack-plugin": "^4.0.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TfTHacker/craft42-readwise/da8ca76bc1d1071abe62064ee662e3f6e8162985/src/gear.png -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TfTHacker/craft42-readwise/da8ca76bc1d1071abe62064ee662e3f6e8162985/src/icon.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Readwise Highlights
10 |
11 |
12 | 13 | 15 | 16 | 17 | 19 | 20 | 21 | 23 | 24 |
25 |
26 |

Readwise Access Token:
27 | 28 | 29 |
You can get your Readwise access token at this address https://readwise.io/access_token.

On that page copy your token and paste it into the field above. Then click the go button to go to the primary view of this extension.
30 |

This prototype is brought
to you by @TfTHacker
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "./style.css" 2 | import {readwiseGetBookList, readwiseGetHighlightsByBookID, readwiseGetRandomHighlight} from "./readwiseStuff"; 3 | import { CraftBlockInsert, CraftImageBlockInsert } from "@craftdocs/craft-extension-api" 4 | 5 | let divBookList: HTMLDivElement; 6 | let divToolbarLower: HTMLDivElement; 7 | let btnRandomHighlight: HTMLButtonElement; 8 | let btnRefreshHighlightList: HTMLButtonElement; 9 | let btnConfigureReadwise: HTMLImageElement; 10 | let divSettingsWrapper: HTMLDivElement; 11 | let inputReadwiseApiToken: HTMLInputElement; 12 | let btnSaveConfig: HTMLDivElement; 13 | let lastBookListQuery: any; 14 | 15 | window.addEventListener("load", async () => { 16 | await initializeUI(); 17 | }) 18 | 19 | 20 | /** 21 | * Generates the UI for the widget 22 | */ 23 | const initializeUI = async ()=> { 24 | divBookList = document.getElementById("book-list"); 25 | divToolbarLower = document.getElementById("toolbar-lower"); 26 | btnRandomHighlight = document.getElementById('btn-random-highlight'); 27 | btnRefreshHighlightList = document.getElementById('btn-refresh-highlights'); 28 | btnConfigureReadwise = document.getElementById("btn-configure-readwise"); 29 | divSettingsWrapper = document.getElementById("config-readwise-setings"); 30 | inputReadwiseApiToken = document.getElementById("readwise-api-key") 31 | btnSaveConfig = document.getElementById("btn-save-config"); 32 | 33 | pleaseWaitLoadingHighlights(); 34 | 35 | // prepare event handlers 36 | btnRandomHighlight.addEventListener("click", async ()=> await insertRandomHighlight() ); 37 | 38 | btnRefreshHighlightList.addEventListener("click", async () => { 39 | divBookList.innerHTML=""; 40 | await listBooks() 41 | }); 42 | 43 | inputReadwiseApiToken?.addEventListener("keyup", () => { 44 | craft.storageApi.put("readwiseToken", inputReadwiseApiToken.value) 45 | if(inputReadwiseApiToken.value.trim()==="") { 46 | btnRefreshHighlightList.style.display=""; 47 | divSettingsWrapper.style.display="inline"; 48 | } 49 | }); 50 | 51 | btnSaveConfig.addEventListener("click", async ()=> await toggleSettingsOnOff()); 52 | 53 | btnConfigureReadwise.addEventListener("click", async ()=> await toggleSettingsOnOff()); 54 | 55 | // initialize UI 56 | const rwToken = await craft.storageApi.get("readwiseToken"); 57 | if (rwToken.status != "error" && rwToken.data != "") { 58 | inputReadwiseApiToken.value = rwToken.data; 59 | btnRefreshHighlightList.style.display = "inline"; 60 | await listBooks(); 61 | } else { 62 | divToolbarLower.style.visibility="hidden"; 63 | divSettingsWrapper.style.display = "inline"; 64 | divBookList.style.height="0px"; 65 | } 66 | } 67 | 68 | /** 69 | * Displays the settings area or hides it 70 | */ 71 | const toggleSettingsOnOff = async ()=> { 72 | divSettingsWrapper.style.display = divSettingsWrapper.style.display==="" ? "inline" : ""; 73 | divBookList.style.height="0px"; 74 | if(divSettingsWrapper.style.display==="") { 75 | divBookList.innerHTML = ""; 76 | divBookList.style.height="500px"; 77 | divToolbarLower.style.visibility="visible"; 78 | await listBooks(); 79 | } else { 80 | divToolbarLower.style.visibility="hidden"; 81 | } 82 | if(inputReadwiseApiToken.value.trim()!="") btnRefreshHighlightList.style.display="inline"; 83 | } 84 | 85 | /** 86 | * Message displayed in the book list area while loading books 87 | */ 88 | const pleaseWaitLoadingHighlights = ()=>{ 89 | divBookList.innerHTML=`
Please wait, loading highlights ...
`; 90 | } 91 | 92 | /** 93 | * Inserts into craft a random highlight from Readwise 94 | */ 95 | const insertRandomHighlight = async ()=> { 96 | const rwToken = await craft.storageApi.get("readwiseToken"); 97 | const highlight = await readwiseGetRandomHighlight( rwToken.data); 98 | const bookInfo = lastBookListQuery.find((b:any)=> b.id.toString() === highlight.book_id.toString() ); 99 | craft.dataApi.addBlocks([{ 100 | type: "textBlock", 101 | hasFocusDecoration: true, 102 | content: [ 103 | { text: highlight.text }, 104 | { text: " - " }, 105 | { text: bookInfo.title, isItalic:true, link: { 106 | type: "url", 107 | url: (highlight.url != null ? highlight.url : (bookInfo.source_url!=null ? bookInfo.source_url : `https://readwise.io/open/${highlight.id}`) ) 108 | }}, 109 | { text: " by " + bookInfo.author + "", isItalic:true}, 110 | ] 111 | }, 112 | { type: "textBlock", content: ""} 113 | ]); 114 | } 115 | 116 | /** 117 | * 118 | * Grabs all highlights for a book and inserts them into Craft 119 | * 120 | * @param bookId ID of the document to retrieve highlights for 121 | * 122 | */ 123 | const insertHighlights = async (bookId : string) => { 124 | const rwToken = await craft.storageApi.get("readwiseToken"); 125 | const highlights = await readwiseGetHighlightsByBookID( rwToken.data, bookId); 126 | const bookInfo = lastBookListQuery.find((b:any)=> b.id.toString() === bookId ); 127 | let output: CraftBlockInsert[] = []; 128 | if(bookInfo.title) 129 | output.push( { type: "textBlock", content: bookInfo.title, style: { textStyle: "title"} } ); 130 | 131 | console.log(bookInfo) 132 | if(bookInfo.cover_image_url) 133 | output.push( { type: "imageBlock", url: bookInfo.cover_image_url } ); 134 | 135 | if(bookInfo.author) 136 | output.push( { type: "textBlock", content: [ { text: "Author:", isBold: true}, {text: " " + bookInfo.author}]} ); 137 | 138 | if(bookInfo.category) 139 | output.push( { type: "textBlock", content: [ { text: "Category:", isBold: true}, {text: " " + bookInfo.category}]} ); 140 | 141 | if(bookInfo.source_url) 142 | output.push( { type: "textBlock", content: [ { text: "Source: ", isBold: true}, 143 | { text: bookInfo.source_url, link: {type: "url", url: bookInfo.source_url} }] }); 144 | 145 | if(bookInfo.tags.length>0) { 146 | const tags = bookInfo.tags.map((t:any)=> "#" + t.name).join(" "); 147 | output.push( { type: "textBlock", content: [ { text: "Tags:", isBold: true}, {text: " " + tags}]} ); 148 | } 149 | output.push( { type: "textBlock", content: [ { text: "Import Date:", isBold: true}, {text: " " + (new Date()).toLocaleDateString() + " " + (new Date()).toLocaleTimeString() }]} ); 150 | 151 | if(bookInfo.last_highlight_at && bookInfo.last_highlight_at != "") 152 | output.push( { type: "textBlock", content: [ { text: "Last Highlight Date:", isBold: true}, {text: " " + (new Date(bookInfo.last_highlight_at)).toLocaleDateString() + " " + (new Date(bookInfo.last_highlight_at)).toLocaleTimeString() }]} ); 153 | 154 | output.push( { type: "textBlock", content: [{ text: `Total Highlights: `, isBold: true}, { text: `${bookInfo.num_highlights}`}]} ); 155 | 156 | const bulletStyle = craft.blockFactory.defaultListStyle("bullet"); 157 | let lastGroupDate: string; 158 | const allHighlights = highlights.results.reverse().forEach( (highlight:any) => { 159 | const highlightDate: Date = new Date(highlight.highlighted_at); 160 | if(lastGroupDate!==getFormattedDate(highlightDate)) { 161 | lastGroupDate = getFormattedDate(highlightDate); 162 | output.push( { type: "textBlock", 163 | content: [ 164 | { 165 | text: getFormattedDate(highlightDate), 166 | link: {type:"dateLink", date: getFormattedDate(highlightDate) } 167 | } 168 | ]} 169 | ); 170 | } 171 | output.push( 172 | craft.blockFactory.textBlock({ 173 | listStyle: bulletStyle, 174 | indentationLevel: 1, 175 | content: [ 176 | { text: highlight.text + " " }, 177 | { text: "link", link: { type: "url", 178 | url: (highlight.url != null ? highlight.url : (bookInfo.source_url!=null ? bookInfo.source_url : `https://readwise.io/open/${highlight.id}`)) } 179 | } 180 | ] 181 | }) 182 | ); 183 | if(highlight.note!="") 184 | output.push( craft.blockFactory.textBlock({ listStyle: bulletStyle, indentationLevel: 2, content: [{text: `Notes: ${highlight.note}`}] }) ); 185 | if(highlight.tags.length>0) { 186 | const tags = highlight.tags.map((t:any)=> "#" + t.name).join(" "); 187 | output.push( craft.blockFactory.textBlock({ listStyle: bulletStyle, indentationLevel: 2, content: [{text: `Tags: ${tags}`}] }) ); 188 | } 189 | }); 190 | console.log(output); 191 | craft.dataApi.addBlocks( output ); 192 | } 193 | 194 | const listBooks = async () => { 195 | pleaseWaitLoadingHighlights(); 196 | const rwToken = await craft.storageApi.get("readwiseToken"); 197 | lastBookListQuery = await readwiseGetBookList(rwToken.data) 198 | let output = ""; 199 | if(lastBookListQuery===null) { 200 | divBookList.innerHTML="Information could not be retrieved from Readwise. Please verify the Readwise Access Token." 201 | return; 202 | } 203 | lastBookListQuery.forEach((e : any) => { 204 | if(e.num_highlights===0) return; 205 | output += `
206 | 207 | 208 |
${e.title} (${e.num_highlights})
209 |
${e.author}
210 |
211 | 212 |
`; 213 | }); 214 | divBookList.innerHTML = output; 215 | document.querySelectorAll(".btn-insert-highlights").forEach(async (i) => { 216 | i.addEventListener("click", async (e) => await insertHighlights(i.id) ); 217 | }); 218 | } 219 | 220 | craft.env.setListener((env) => { 221 | switch (env.colorScheme) { 222 | case "dark": 223 | document.body.classList.add("dark"); 224 | break; 225 | case "light": 226 | document.body.classList.remove("dark"); 227 | break; 228 | } 229 | }) 230 | 231 | // Function borrowed from our good friends at: https://github.com/deadlyhifi/craft-x-insert-date 232 | function getFormattedDate(date: Date | null = null) { 233 | const selectedDate = date ? date : new Date(); 234 | return `${selectedDate.getFullYear()}-${( 235 | "0" + 236 | (selectedDate.getMonth() + 1) 237 | ).slice(-2)}-${("0" + selectedDate.getDate()).slice(-2)}`; 238 | } -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "TfTHacker.craft42.readwise", 3 | "name": "Readwise Highlights", 4 | "fileName": "craft42-readwise", 5 | "author" : "@TfThacker", 6 | "author-email" : "@TfTHacker", 7 | "description" : "This is a prototype (v4) developed by @TfTHacker and is not affliated with the Readwise company. This extension imports highilghts into current document from Readwise.", 8 | "api": "0.0.4", 9 | "main": "index.html" 10 | } 11 | -------------------------------------------------------------------------------- /src/readwiseStuff.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * List of last 1000 books highlighted in Readwise 4 | * 5 | * @param rwToken Access token for api call 6 | * @returns list of last 1000 items from readwise 7 | */ 8 | export const readwiseGetBookList = async (rwToken : string) => { 9 | try { 10 | const bookList = await fetch('https://readwise.io/api/v2/books/?page_size=1000', { 11 | headers: { 12 | "Authorization": "Token " + rwToken 13 | } 14 | }) 15 | // sort by date, newest to oldest 16 | return (await bookList.json()).results.sort( (a:any,b:any)=>{ 17 | const c = new Date(a.last_highlight_at).getTime(); 18 | const d = new Date(b.last_highlight_at).getTime(); 19 | return d - c; 20 | }); 21 | } catch (error) { 22 | return null; 23 | } 24 | } 25 | 26 | /** 27 | * 28 | * Retrieves all highlights for a given document 29 | * 30 | * @param rwToken Access token for api call 31 | * @param bookId Retrieve highlights for this book ID 32 | * @returns array of highlights 33 | */ 34 | export const readwiseGetHighlightsByBookID = async (rwToken : string, bookId: string) => { 35 | const highlightList = await fetch(`https://readwise.io/api/v2/highlights/?book_id=${bookId}`, { 36 | headers: { 37 | "Authorization": "Token " + rwToken, 38 | } 39 | }); 40 | return await highlightList.json(); 41 | } 42 | 43 | 44 | /** 45 | * 46 | * Gets detailed information on a document by its book id 47 | * 48 | * @param rwToken Access token for api call 49 | * @param bookId Retrieve highlights for this book ID 50 | * @returns details of a document 51 | */ 52 | export const readwiseGetBookDetails= async (rwToken : string, bookId: string) => { 53 | const highlightList = await fetch(`https://readwise.io/api/v2/highlights/?book_id=${bookId}`, { 54 | headers: { 55 | "Authorization": "Token " + rwToken, 56 | } 57 | }); 58 | return await highlightList.json(); 59 | } 60 | 61 | 62 | /** 63 | * 64 | * generates a random number 65 | * 66 | * @param max highest number 67 | * @returns number 68 | */ 69 | function getRandomInt(max: number) { 70 | return Math.floor(Math.random() * max); 71 | } 72 | 73 | 74 | /** 75 | * 76 | * Grabs a random highlight from readwise 77 | * 78 | * @param rwToken Access token for api call 79 | * @returns 80 | */ 81 | export const readwiseGetRandomHighlight =async (rwToken : string) => { 82 | const highlights = await fetch(`https://readwise.io/api/v2/highlights/?page_size=500`, { 83 | headers: { 84 | "Authorization": "Token " + rwToken, 85 | } 86 | }); 87 | const respone = await highlights.json(); 88 | const randomIndex = getRandomInt(respone.results.length); 89 | return respone.results[randomIndex]; 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TfTHacker/craft42-readwise/da8ca76bc1d1071abe62064ee662e3f6e8162985/src/refresh.png -------------------------------------------------------------------------------- /src/snooker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TfTHacker/craft42-readwise/da8ca76bc1d1071abe62064ee662e3f6e8162985/src/snooker.png -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | overflow-y: hidden; 7 | } 8 | 9 | body.dark { 10 | background-color: #222222; 11 | color: silver; 12 | } 13 | 14 | .extension-header { 15 | padding-top: 15px; 16 | padding-bottom: 10px; 17 | font-size: 16pt; 18 | font-weight: bold; 19 | color: darkcyan; 20 | } 21 | 22 | #book-list { 23 | overflow-x:hidden; 24 | overflow-y:auto; 25 | width:255px; 26 | height:500px; 27 | border-color: darkcyan !important; 28 | border-width: 1px; 29 | border-style:solid; 30 | } 31 | 32 | .dark #book-list { 33 | border-color: darkslategray !important; 34 | } 35 | 36 | #book-list::-webkit-scrollbar { 37 | width: 8px; 38 | } 39 | 40 | #book-list::-webkit-scrollbar-track { 41 | background-color: darkcyan; 42 | } 43 | 44 | .dark #book-list::-webkit-scrollbar-track { 45 | background-color: darkslategray; 46 | } 47 | 48 | #book-list::-webkit-scrollbar-thumb { 49 | box-shadow: inset 0 0 6px lightblue; 50 | } 51 | 52 | .readwise-book-container { 53 | padding-bottom:4px; 54 | width:250px; 55 | display: flex; 56 | border-bottom-style: inset; 57 | border-color: lightblue; 58 | border-bottom-width:1px; 59 | padding-top: 3px; 60 | } 61 | 62 | .dark .readwise-book-container { 63 | border-color: darkslategray; 64 | } 65 | 66 | .book-images-wrapper { 67 | width: 50px; 68 | } 69 | 70 | .book-images { 71 | width: 45px; 72 | margin-left: 5px; 73 | border-radius: 10%; 74 | } 75 | 76 | .book-info { 77 | padding-left:5px; 78 | width: 100%; 79 | } 80 | 81 | .btn-insert-highlights { 82 | filter: invert(0.95); 83 | width: 20px !important; 84 | padding-right: 4px; 85 | position: relative; 86 | float: right; 87 | } 88 | 89 | .dark .btn-insert-highlights { 90 | filter: invert(0.2); 91 | } 92 | 93 | 94 | .btn-insert-highlights:hover { 95 | filter: invert(0.85); 96 | } 97 | 98 | #btn-save-config, .button-refresh-highlights { 99 | background-color: lightblue; 100 | border: none; 101 | text-align: center; 102 | text-decoration: none; 103 | font-size: 12px; 104 | border-radius: 6px; 105 | cursor: pointer; 106 | height: 25px; 107 | } 108 | 109 | .button-refresh-highlights { 110 | width: 145px; 111 | display: none; 112 | } 113 | 114 | .dark #btn-save-config, .dark .button-refresh-highlights { 115 | background-color: darkcyan; 116 | color: white; 117 | } 118 | 119 | #btn-save-config:hover { 120 | background-color: rgb(202, 235, 247); 121 | } 122 | 123 | .dark #btn-save-config:hover { 124 | background-color: rgb(34, 160, 160); 125 | } 126 | 127 | .toolbar-lower { 128 | visibility: hidden; 129 | } 130 | 131 | .button-toolbar-lower { 132 | padding-left: 20px; 133 | } 134 | 135 | .button-toolbar-lower-image { 136 | height: 25px; 137 | position: relative; 138 | top:-2px; 139 | filter: invert(0.6); 140 | cursor: pointer; 141 | padding-bottom: 0px; 142 | } 143 | 144 | .button-toolbar-lower-image:hover { 145 | filter: invert(0.4); 146 | } 147 | 148 | #gear-image { 149 | height: 22px; 150 | padding-top: 2px; 151 | } 152 | 153 | #config-readwise-setings { 154 | display: none; 155 | margin-bottom: 20px; 156 | } 157 | 158 | 159 | #btn-configure-readwise { 160 | filter: invert(0.8); 161 | } 162 | 163 | .dark a { 164 | color: aqua 165 | } 166 | 167 | 168 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "amd", 7 | "target": "es6", 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "importHelpers": true, 11 | "lib": [ 12 | "dom", 13 | "esnext" 14 | ], 15 | "typeRoots": [ 16 | "./node_modules/@craftdocs/craft-extension-api" 17 | ] 18 | }, 19 | "include": [ 20 | "src" 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const manifestJson = require('./src/manifest.json') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin') 5 | const ZipPlugin = require('zip-webpack-plugin') 6 | const CopyPlugin = require('copy-webpack-plugin') 7 | const { CraftExtensionApiPlugin } = require('@craftdocs/craft-extension-api-sdk') 8 | 9 | module.exports = (env, argv) => { 10 | const isProd = argv.mode === 'production' 11 | 12 | return { 13 | entry: { 14 | app: './src/index.ts' 15 | }, 16 | output: { 17 | filename: '[name].js', 18 | path: path.resolve(__dirname, 'dist'), 19 | publicPath: "/" 20 | }, 21 | resolve: { 22 | extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'] 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.tsx?$/, 28 | use: 'ts-loader', 29 | exclude: /node_modules/ 30 | }, 31 | // Enables including CSS by doing "import './file.css'" in your TypeScript code 32 | { 33 | test: /\.css$/, 34 | use: ['style-loader', 'css-loader'] 35 | }, 36 | // Allows you to use import './file.png'" in your code to get a data URI 37 | { 38 | test: /\.(svg|png|bmp|jpg|jpeg|webp|gif)$/i, 39 | use: [ 40 | { 41 | loader: 'url-loader', 42 | options: { 43 | limit: 100000000, 44 | }, 45 | }, 46 | ], 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | new CraftExtensionApiPlugin(), 52 | new HtmlWebpackPlugin({ 53 | inject: 'body', 54 | template: path.resolve(__dirname, 'src/index.html'), 55 | chunks: ['app'] 56 | }), 57 | isProd && new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/app/]), 58 | isProd && new CopyPlugin({ 59 | patterns: [ 60 | { from: './src/manifest.json' }, 61 | { from: './src/icon.png' } 62 | ], 63 | }), 64 | isProd && new ZipPlugin({ 65 | filename: manifestJson.fileName, 66 | extension: 'craftx', 67 | include: [ 68 | 'app.js.LICENSE.txt', 69 | 'index.html', 70 | 'manifest.json', 71 | 'icon.png' 72 | ] 73 | }) 74 | ].filter(Boolean) 75 | } 76 | } 77 | --------------------------------------------------------------------------------