gatsby-source-notion-churnotion 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # plugin
2
+
3
+ The main focus of the overall tutorial. This contains the whole source plugin.
@@ -0,0 +1,9 @@
1
+ import { NotionToMarkdown } from "notion-to-md";
2
+ /**
3
+ * Page to Markdown Instance
4
+ */
5
+ export declare const n2m: NotionToMarkdown;
6
+ /**
7
+ * Notion API Instance
8
+ */
9
+ export declare const instance: import("axios").AxiosInstance;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.instance = exports.n2m = void 0;
7
+ const client_1 = require("@notionhq/client");
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const notion_to_md_1 = require("notion-to-md");
10
+ const constants_1 = require("../constants");
11
+ const notion = new client_1.Client({
12
+ auth: process.env.GATSBY_INTEGRATION_TOKEN,
13
+ });
14
+ /**
15
+ * Page to Markdown Instance
16
+ */
17
+ exports.n2m = new notion_to_md_1.NotionToMarkdown({ notionClient: notion });
18
+ /**
19
+ * Notion API Instance
20
+ */
21
+ exports.instance = axios_1.default.create({
22
+ baseURL: `https://api.notion.com/v1/`,
23
+ headers: {
24
+ "Content-Type": `application/json`,
25
+ "Notion-Version": constants_1.NOTION_API_VERSION,
26
+ Authorization: `Bearer ${process.env.GATSBY_INTEGRATION_TOKEN}`,
27
+ },
28
+ });
@@ -0,0 +1,2 @@
1
+ import { IGetPagesParams } from "../types";
2
+ export declare const getPages: ({ databaseId, reporter, getCache, actions, createNode, createNodeId, createParentChildLink, getNode, }: IGetPagesParams) => Promise<void>;
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getPages = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const constants_1 = require("../constants");
9
+ const fetchData_1 = require("../util/fetchData");
10
+ const imageProcessor_1 = require("../util/imageProcessor");
11
+ const slugify_1 = require("../util/slugify");
12
+ const connector_1 = require("./connector");
13
+ const getPages = async ({ databaseId, reporter, getCache, actions, createNode, createNodeId, createParentChildLink, getNode, }) => {
14
+ let hasMore = true;
15
+ /**
16
+ * 데이터베이스 내에 페이지들을 읽어서 재귀적으로 추가하는 서브 메서드드
17
+ * @param databaseId 데이터베이스 아이디
18
+ * @param parentCategoryId 부모 데이터베이스 아이디
19
+ */
20
+ const processDatabase = async (databaseId, parentCategoryId = null) => {
21
+ try {
22
+ while (hasMore) {
23
+ const databaseUrl = `databases/${databaseId}/query`;
24
+ const body = {};
25
+ const result = await (0, fetchData_1.fetchPostWithRetry)(databaseUrl, body);
26
+ if (result?.results?.length) {
27
+ reporter.info(`[SUCCESS] total pages > ${result.results.length}`);
28
+ }
29
+ for (const page of result.results) {
30
+ reporter.info(`[CHECK!!!] page: ${page.id}`);
31
+ const pageUrl = `blocks/${page.id}/children?page_size=100`;
32
+ // 페이지 데이터
33
+ const pageData = await (0, fetchData_1.fetchGetWithRetry)(pageUrl);
34
+ if (pageData.results[0].type === `child_database`) {
35
+ const categoryJsonData = pageData.results[0];
36
+ const title = categoryJsonData.child_database?.title || `Unnamed Category`;
37
+ const slug = (0, slugify_1.slugify)(title);
38
+ if (!title) {
39
+ reporter.warn(`[WARNING] Category without a title detected: ${categoryJsonData.id}`);
40
+ }
41
+ const nodeId = createNodeId(`${categoryJsonData.id}-category`);
42
+ const categoryNode = {
43
+ id: nodeId,
44
+ category_name: title,
45
+ parent_id: parentCategoryId,
46
+ slug: slug || `no-title-${categoryJsonData.id}`,
47
+ children: [],
48
+ internal: {
49
+ type: constants_1.NODE_TYPE.Category,
50
+ contentDigest: crypto_1.default
51
+ .createHash(`md5`)
52
+ .update(JSON.stringify(categoryJsonData))
53
+ .digest(`hex`),
54
+ },
55
+ };
56
+ createNode(categoryNode);
57
+ if (parentCategoryId && categoryNode) {
58
+ const parentNode = getNode(parentCategoryId); // Gatsby에서 노드를 검색
59
+ if (parentNode) {
60
+ createParentChildLink({
61
+ parent: parentNode,
62
+ child: categoryNode,
63
+ });
64
+ reporter.info(`[SUCCESS] Linked parent: ${parentNode.category_name} -> child: ${categoryNode.category_name}`);
65
+ }
66
+ else {
67
+ reporter.warn(`[WARNING] Parent node not found for ID: ${parentCategoryId}`);
68
+ }
69
+ }
70
+ await processDatabase(categoryJsonData.id, nodeId);
71
+ }
72
+ else {
73
+ // 페이지인 경우
74
+ const title = page.properties?.[`이름`]?.title?.[0]?.plain_text || `Unnamed`;
75
+ const slug = (0, slugify_1.slugify)(title);
76
+ if (!title) {
77
+ reporter.warn(`[WARNING] Category without a title detected: ${page.id}`);
78
+ }
79
+ const nodeId = createNodeId(`${page.id}-page`);
80
+ // Tag 노드 만들기
81
+ if (page.properties.tags && page.properties.tags.multi_select) {
82
+ page.properties.tags.multi_select.map((tagData) => createNode({
83
+ id: createNodeId(`${tagData.id}-tag`),
84
+ tag_name: tagData.name,
85
+ color: tagData.color,
86
+ internal: {
87
+ type: constants_1.NODE_TYPE.Tag,
88
+ contentDigest: crypto_1.default
89
+ .createHash(`md5`)
90
+ .update(JSON.stringify(tagData.id))
91
+ .digest(`hex`),
92
+ },
93
+ }));
94
+ }
95
+ const bookId = page.properties?.book?.relation?.[0]?.id || null;
96
+ const markdownContent = await connector_1.n2m.pageToMarkdown(page.id);
97
+ await (0, imageProcessor_1.processBlocks)(markdownContent, actions, getCache, createNodeId, reporter);
98
+ const postNode = {
99
+ id: nodeId,
100
+ category_id: parentCategoryId,
101
+ book_id: bookId,
102
+ title: title,
103
+ content: markdownContent,
104
+ create_date: page.created_time,
105
+ update_date: page.last_edited_time,
106
+ version: page.properties?.version?.rich_text?.[0]?.plain_text || null,
107
+ description: null,
108
+ slug: slug || `no-title-${nodeId}`,
109
+ children: [],
110
+ internal: {
111
+ type: constants_1.NODE_TYPE.Post,
112
+ contentDigest: crypto_1.default
113
+ .createHash(`md5`)
114
+ .update(JSON.stringify(nodeId))
115
+ .digest(`hex`),
116
+ },
117
+ };
118
+ await createNode(postNode);
119
+ if (parentCategoryId && postNode) {
120
+ const parentNode = getNode(parentCategoryId); // Gatsby에서 노드를 검색
121
+ if (parentNode) {
122
+ createParentChildLink({
123
+ parent: parentNode,
124
+ child: postNode,
125
+ });
126
+ reporter.info(`[SUCCESS] Linked parent: ${parentNode.category_name} -> child: ${postNode.title}`);
127
+ }
128
+ else {
129
+ reporter.warn(`[WARNING] Parent node not found for ID: ${parentCategoryId}`);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ hasMore = result.has_more;
135
+ }
136
+ }
137
+ catch (error) {
138
+ reporter.error(`[ERROR] fetching page`);
139
+ }
140
+ };
141
+ await processDatabase(databaseId);
142
+ };
143
+ exports.getPages = getPages;
@@ -0,0 +1,9 @@
1
+ export declare const NOTION_API_VERSION = "2022-06-28";
2
+ export declare const NOTION_LIMIT_ERROR = 429;
3
+ export declare const PLUGIN_NAME = "gatsby-source-notion-churnotion";
4
+ export declare const NODE_TYPE: {
5
+ Post: string;
6
+ Category: string;
7
+ Tag: string;
8
+ Book: string;
9
+ };
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NODE_TYPE = exports.PLUGIN_NAME = exports.NOTION_LIMIT_ERROR = exports.NOTION_API_VERSION = void 0;
4
+ exports.NOTION_API_VERSION = `2022-06-28`;
5
+ exports.NOTION_LIMIT_ERROR = 429;
6
+ exports.PLUGIN_NAME = `gatsby-source-notion-churnotion`;
7
+ //////////////////////////////////////////////////
8
+ exports.NODE_TYPE = {
9
+ Post: `Churnotion`,
10
+ Category: `NCategory`,
11
+ Tag: `NTag`,
12
+ Book: `NBook`,
13
+ };
@@ -0,0 +1,2 @@
1
+ import { GatsbyNode } from "gatsby";
2
+ export declare const createSchemaCustomization: GatsbyNode[`createSchemaCustomization`];
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSchemaCustomization = void 0;
4
+ const constants_1 = require("./constants");
5
+ const createSchemaCustomization = ({ actions }) => {
6
+ const { createTypes } = actions;
7
+ createTypes(`
8
+ type ${constants_1.NODE_TYPE.Post} implements Node {
9
+ id: ID!
10
+ category_id: ${constants_1.NODE_TYPE.Category} @link(by: "id")
11
+ tags: ${constants_1.NODE_TYPE.Tag} @link(by: "id")
12
+ book_id: ${constants_1.NODE_TYPE.Book} @link(by: "id")
13
+ title: String
14
+ content: [JSON]
15
+ create_date: Date! @dateformat
16
+ update_date: Date! @dateformat
17
+ version: Int
18
+ description: String
19
+ slug: String
20
+ }
21
+
22
+ type ${constants_1.NODE_TYPE.Tag} implements Node {
23
+ id: ID!
24
+ tag_name: String!
25
+ color: String!
26
+ }
27
+
28
+ type ${constants_1.NODE_TYPE.Category} implements Node {
29
+ id: ID!
30
+ parent_id: ${constants_1.NODE_TYPE.Category} @link(by: "id")
31
+ category_name: String!
32
+ slug: String!
33
+ }
34
+
35
+ type ${constants_1.NODE_TYPE.Book} implements Node {
36
+ id: ID!
37
+ book_name: String!
38
+ create_date: Date! @dateformat
39
+ update_date: Date! @dateformat
40
+ }
41
+ `);
42
+ };
43
+ exports.createSchemaCustomization = createSchemaCustomization;
@@ -0,0 +1,4 @@
1
+ export type { IPluginOptions } from "./types";
2
+ export { onPluginInit } from "./on-plugin-init";
3
+ export { sourceNodes } from "./source-nodes";
4
+ export { createSchemaCustomization } from "./createSchemaCustomization";
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSchemaCustomization = exports.sourceNodes = exports.onPluginInit = void 0;
4
+ var on_plugin_init_1 = require("./on-plugin-init");
5
+ Object.defineProperty(exports, "onPluginInit", { enumerable: true, get: function () { return on_plugin_init_1.onPluginInit; } });
6
+ var source_nodes_1 = require("./source-nodes");
7
+ Object.defineProperty(exports, "sourceNodes", { enumerable: true, get: function () { return source_nodes_1.sourceNodes; } });
8
+ var createSchemaCustomization_1 = require("./createSchemaCustomization");
9
+ Object.defineProperty(exports, "createSchemaCustomization", { enumerable: true, get: function () { return createSchemaCustomization_1.createSchemaCustomization; } });
@@ -0,0 +1,2 @@
1
+ import type { GatsbyNode } from "gatsby";
2
+ export declare const onPluginInit: GatsbyNode[`onPluginInit`];
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.onPluginInit = void 0;
4
+ const onPluginInit = ({ reporter }) => {
5
+ reporter.info(`Example plugin loaded...`);
6
+ };
7
+ exports.onPluginInit = onPluginInit;
@@ -0,0 +1,2 @@
1
+ import type { GatsbyNode } from "gatsby";
2
+ export declare const sourceNodes: GatsbyNode["sourceNodes"];
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sourceNodes = void 0;
4
+ const getPage_1 = require("./api/getPage");
5
+ const sourceNodes = async (gatsbyApi, options) => {
6
+ const { actions, reporter, createNodeId, getNode, getCache } = gatsbyApi;
7
+ const { createNode, createParentChildLink } = actions;
8
+ const { token, databaseId } = options;
9
+ if (!token || !databaseId) {
10
+ reporter.error(`[ERROR] Missing Notion API token or database ID.`);
11
+ return;
12
+ }
13
+ reporter.info(`[INFO] Fetching pages from Notion database...`);
14
+ try {
15
+ await (0, getPage_1.getPages)({
16
+ token,
17
+ databaseId,
18
+ reporter,
19
+ getCache,
20
+ actions,
21
+ createNode,
22
+ createNodeId,
23
+ createParentChildLink,
24
+ getNode,
25
+ });
26
+ }
27
+ catch (e) {
28
+ reporter.error(`[ERROR] ${e}`);
29
+ }
30
+ };
31
+ exports.sourceNodes = sourceNodes;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * POST request with retry logic using axios instance
3
+ * @param {string} url - API endpoint URL
4
+ * @param {any} body - Request body
5
+ * @param {number} tryCount - Current retry count
6
+ * @param {number} maxRetries - Maximum retry attempts
7
+ * @returns {Promise<any>}
8
+ */
9
+ export declare const fetchPostWithRetry: (url: string, body: any, tryCount?: number, maxRetries?: number) => Promise<any>;
10
+ /**
11
+ * General GET/PUT/DELETE request with retry logic using axios instance
12
+ * @param {string} url - API endpoint URL
13
+ * @param {any} options - Axios request options
14
+ * @param {number} tryCount - Current retry count
15
+ * @param {number} maxRetries - Maximum retry attempts
16
+ * @returns {Promise<any>}
17
+ */
18
+ export declare const fetchGetWithRetry: (url: string, options?: any, tryCount?: number, maxRetries?: number) => Promise<any>;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchGetWithRetry = exports.fetchPostWithRetry = void 0;
4
+ const connector_1 = require("../api/connector");
5
+ const constants_1 = require("../constants");
6
+ const timeLimit_1 = require("./timeLimit");
7
+ /**
8
+ * POST request with retry logic using axios instance
9
+ * @param {string} url - API endpoint URL
10
+ * @param {any} body - Request body
11
+ * @param {number} tryCount - Current retry count
12
+ * @param {number} maxRetries - Maximum retry attempts
13
+ * @returns {Promise<any>}
14
+ */
15
+ const fetchPostWithRetry = async (url, body, tryCount = 0, maxRetries = 5) => {
16
+ try {
17
+ const response = await connector_1.instance.post(url, body);
18
+ // Return data if the request was successful
19
+ return response.data;
20
+ }
21
+ catch (error) {
22
+ const status = error.response?.status;
23
+ const message = error.response?.data?.message || `Unknown error`;
24
+ // Handle rate limit errors with exponential backoff
25
+ if (status === constants_1.NOTION_LIMIT_ERROR && tryCount < maxRetries) {
26
+ const delay = Math.pow(2, tryCount) * 1000; // Exponential backoff: 1s, 2s, 4s...
27
+ console.log(`[${constants_1.PLUGIN_NAME}] Rate limit hit. Retrying in ${delay / 1000}s...`);
28
+ await (0, timeLimit_1.sleep)(delay);
29
+ return (0, exports.fetchPostWithRetry)(url, body, tryCount + 1, maxRetries);
30
+ }
31
+ // Handle unexpected errors with retry
32
+ if (tryCount < maxRetries) {
33
+ const delay = Math.pow(2, tryCount) * 1000; // Exponential backoff
34
+ console.log(`[${constants_1.PLUGIN_NAME}] Unexpected error. Retrying in ${delay / 1000}s...`);
35
+ await (0, timeLimit_1.sleep)(delay);
36
+ return (0, exports.fetchPostWithRetry)(url, body, tryCount + 1, maxRetries);
37
+ }
38
+ // Log and throw the error if all retries fail
39
+ console.error(`[${constants_1.PLUGIN_NAME}] Failed after ${tryCount} retries:`, message);
40
+ throw error;
41
+ }
42
+ };
43
+ exports.fetchPostWithRetry = fetchPostWithRetry;
44
+ /**
45
+ * General GET/PUT/DELETE request with retry logic using axios instance
46
+ * @param {string} url - API endpoint URL
47
+ * @param {any} options - Axios request options
48
+ * @param {number} tryCount - Current retry count
49
+ * @param {number} maxRetries - Maximum retry attempts
50
+ * @returns {Promise<any>}
51
+ */
52
+ const fetchGetWithRetry = async (url, options = {}, tryCount = 0, maxRetries = 5) => {
53
+ try {
54
+ const response = await connector_1.instance.get(url);
55
+ // Return data if the request was successful
56
+ return response.data;
57
+ }
58
+ catch (error) {
59
+ const status = error.response?.status;
60
+ const message = error.response?.data?.message || `Unknown error`;
61
+ // Handle rate limit errors with exponential backoff
62
+ if (status === constants_1.NOTION_LIMIT_ERROR && tryCount < maxRetries) {
63
+ const delay = Math.pow(2, tryCount) * 1000; // Exponential backoff: 1s, 2s, 4s...
64
+ console.log(`[${constants_1.PLUGIN_NAME}] Rate limit hit. Retrying in ${delay / 1000}s...`);
65
+ await (0, timeLimit_1.sleep)(delay);
66
+ return (0, exports.fetchGetWithRetry)(url, options, tryCount + 1, maxRetries);
67
+ }
68
+ // Handle unexpected errors with retry
69
+ if (tryCount < maxRetries) {
70
+ const delay = Math.pow(2, tryCount) * 1000; // Exponential backoff
71
+ console.log(`[${constants_1.PLUGIN_NAME}] Unexpected error. Retrying in ${delay / 1000}s...`);
72
+ await (0, timeLimit_1.sleep)(delay);
73
+ return (0, exports.fetchGetWithRetry)(url, options, tryCount + 1, maxRetries);
74
+ }
75
+ // Log and throw the error if all retries fail
76
+ console.error(`[${constants_1.PLUGIN_NAME}] Failed after ${tryCount} retries:`, message);
77
+ throw error;
78
+ }
79
+ };
80
+ exports.fetchGetWithRetry = fetchGetWithRetry;
@@ -0,0 +1,3 @@
1
+ import { Actions, GatsbyCache, Reporter } from "gatsby";
2
+ import { MdBlock } from "notion-to-md/build/types";
3
+ export declare const processBlocks: (blocks: MdBlock[], actions: Actions, getCache: (this: void, id: string) => GatsbyCache, createNodeId: (this: void, input: string) => string, reporter: Reporter) => Promise<void>;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.processBlocks = void 0;
7
+ const gatsby_source_filesystem_1 = require("gatsby-source-filesystem");
8
+ const path_1 = __importDefault(require("path"));
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const processBlocks = async (blocks, actions, getCache, createNodeId, reporter) => {
11
+ const { createNode } = actions;
12
+ for (const block of blocks) {
13
+ // 이미지 블록 처리
14
+ if (block.type === `image` &&
15
+ typeof block.parent === `string` &&
16
+ block.parent.includes(`http`)) {
17
+ const match = block.parent.match(/\((https?:\/\/.*\.(?:png|jpg|jpeg|gif|webp))\)/);
18
+ if (match) {
19
+ const imageUrl = match[1]; // 이미지 URL 추출
20
+ try {
21
+ // 해싱된 파일명 생성
22
+ const fileExtension = path_1.default.extname(imageUrl); // 확장자 추출
23
+ const hashedFileName = crypto_1.default.createHash(`md5`).update(imageUrl).digest(`hex`) +
24
+ fileExtension;
25
+ // 파일 생성
26
+ const fileNode = await (0, gatsby_source_filesystem_1.createRemoteFileNode)({
27
+ url: imageUrl,
28
+ parentNodeId: block.blockId, // 블록 ID를 부모로 설정
29
+ getCache,
30
+ createNode,
31
+ createNodeId,
32
+ name: hashedFileName,
33
+ });
34
+ if (fileNode) {
35
+ const relativePath = `/static/images/${hashedFileName}`; // 해싱된 파일명을 사용
36
+ block.parent = `![${hashedFileName}](${relativePath})`; // Markdown 형식으로 업데이트
37
+ reporter.info(`[SUCCESS] Updated block with new hashed image path: ${block.parent}`);
38
+ }
39
+ }
40
+ catch (error) {
41
+ reporter.warn(`[WARNING] Failed to download image: ${imageUrl}`);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ };
47
+ exports.processBlocks = processBlocks;
@@ -0,0 +1 @@
1
+ export declare const slugify: (text: string) => string;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.slugify = void 0;
4
+ const slugify = (text) => {
5
+ return text.toLowerCase().replace(/\s+/g, `-`);
6
+ };
7
+ exports.slugify = slugify;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Sleep for a given number of milliseconds
3
+ * @param {number} ms - Milliseconds to sleep
4
+ */
5
+ export declare const sleep: (ms: number) => Promise<unknown>;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sleep = void 0;
4
+ /**
5
+ * Sleep for a given number of milliseconds
6
+ * @param {number} ms - Milliseconds to sleep
7
+ */
8
+ const sleep = (ms) => {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ };
11
+ exports.sleep = sleep;
package/gatsby-node.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require(`./dist/gatsby-node`);
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "gatsby-source-notion-churnotion",
3
+ "description": "Gatsby plugin that can connect with One Notion Database RECURSIVELY using official API",
4
+ "version": "1.0.0",
5
+ "skipLibCheck": true,
6
+ "license": "0BSD",
7
+ "main": "./dist/gatsby-node.js",
8
+ "files": [
9
+ "./dist/*",
10
+ "gatsby-node.js"
11
+ ],
12
+ "scripts": {
13
+ "clean": "del-cli dist",
14
+ "build": "tsc",
15
+ "develop": "tsc --watch",
16
+ "test": "jest",
17
+ "prepare": "npm run clean && npm run build"
18
+ },
19
+ "keywords": [
20
+ "gatsby",
21
+ "gatsby-plugin",
22
+ "notion",
23
+ "gatsby-notion",
24
+ "recursive"
25
+ ],
26
+ "author": "Churnobyl <tjdcjfals3@naver.com> (https://github.com/Churnobyl)",
27
+ "homepage": "https://github.com/Churnobyl/gatsby-source-notion-churnotion",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/Churnobyl/gatsby-source-notion-churnotion"
31
+ },
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "dependencies": {
36
+ "@notionhq/client": "^2.2.15",
37
+ "@types/node": "^22.10.2",
38
+ "axios": "^1.7.9",
39
+ "gatsby-source-filesystem": "^5.14.0",
40
+ "gatsby-transformer-json": "^5.14.0",
41
+ "node-fetch": "^3.3.2",
42
+ "notion-to-md": "^3.1.1",
43
+ "typescript": "^5.7.2"
44
+ },
45
+ "devDependencies": {
46
+ "gatsby": "^5.14.0"
47
+ },
48
+ "peerDependencies": {
49
+ "gatsby": "^5.14.0",
50
+ "react": "^18.0.0",
51
+ "react-dom": "^18.0.0"
52
+ }
53
+ }