Skip to content

Commit d3ae33b

Browse files
committed
Initial commit
0 parents  commit d3ae33b

File tree

9 files changed

+2750
-0
lines changed

9 files changed

+2750
-0
lines changed

.github/workflows/nodejs.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Node CI
2+
3+
on: [push]
4+
5+
jobs:
6+
build:
7+
8+
runs-on: ubuntu-latest
9+
10+
strategy:
11+
matrix:
12+
node-version: [8.x, 10.x, 12.x]
13+
14+
steps:
15+
- uses: actions/checkout@v1
16+
- name: Use Node.js ${{ matrix.node-version }}
17+
uses: actions/setup-node@v1
18+
with:
19+
node-version: ${{ matrix.node-version }}
20+
- name: npm install, build, and test
21+
run: |
22+
npm ci
23+
npm run build --if-present
24+
npm test
25+
env:
26+
CI: true

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
.DS_Store

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 Stackbit Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# sourcebit-target-hugo
2+
3+
[![npm version](https://badge.fury.io/js/sourcebit-target-hugo.svg)](https://badge.fury.io/js/sourcebit-target-hugo)
4+
5+
> A Sourcebit plugin for the [Hugo](https://gohugo.io/) static site generator
6+
7+
## 👩‍🏫 Introduction
8+
9+
This plugin writes content from any Sourcebit data source into files compatible with the Jekyll static site generator.
10+
11+
## 🏗 Installation
12+
13+
To install the plugin and add it to your project, run:
14+
15+
```
16+
npm install sourcebit-target-hugo --save
17+
```
18+
19+
> 💡 You don't need to run this command if you start Sourcebit using the [interactive setup process](#%EF%B8%8F-interactive-setup-process), as the CLI will install the plugin for you and add it as a dependency to your project.
20+
21+
## ⚙️ Configuration
22+
23+
The plugin accepts the following configuration parameters. They can be supplied in any of the following ways:
24+
25+
- In the `options` object of the plugin configuration block inside `sourcebit.js`, with the value of the _Property_ column as a key;
26+
- As an environment variable named after the _Env variable_ column, when running the `sourcebit fetch` command;
27+
- As part of a `.env` file, with the value of the _Env variable_ column separated by the value with an equals sign (e.g. `MY_VARIABLE=my-value`);
28+
- As a CLI parameter, when running the `sourcebit fetch` command, using the value of the _Parameter_ column as the name of the parameter (e.g. `sourcebit fetch --my-parameter`).
29+
30+
| Property | Type | Visibility | Default value | Env variable | Parameter | Description |
31+
| ------------------ | -------- | ---------- | ------------- | ------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32+
| `fullAssetObjects` | Boolean | Public | `false` | | | By default, values of fields that reference assets will be written as a string containing just the asset URL. To get a full [asset object](https://github.com/stackbithq/sourcebit/wiki/Data-normalization#assets) instead, set `fullAssetObjects` to `true`. |
33+
| `writeFile` | Function | Public | | | | A function that computes the files to be created, as well as their location, format and contents (see below for more details). |
34+
35+
The `writeFile` function is invoked on each entry from the `objects` data bucket, with the following parameters:
36+
37+
- `entry` (Object): An entry from the `objects` data bucket
38+
- `utils` (Object): An object containing utility methods:
39+
- `slugify` (Function): Creates a filename-friendly version of any string (e.g. `utils.slugify('Hello, Sourcebit friends!') === 'hello-sourcebit-friends'`)
40+
41+
The return value of this function determines whether the entry being evaluated will be written to a file and, if so, defines the path, the format and the contents of the file.
42+
43+
To write a file for an entry, the return value should be an object with a `content`, `format` and `path` properties. The nature of these properties may vary slightly based on the value of `format`, as shown in the table below.
44+
45+
| `format` | `content` | `path` | Description |
46+
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------- |
47+
| `frontmatter-md` | Object containing a `frontmatter` and `body` properties, which will be written to the file's frontmatter and content body, respectively | The absolute path to the file. Must end with `.md`. | Writes a Markdown file with a YAML frontmatter. |
48+
| `yml` | Object to be written as YAML | The absolute path to the file. Must end with `.yaml` or `.yml` | Writes a YAML file. |
49+
| `json` | Object to be written as JSON | The absolute path to the file. Must end with `.json`. | Writes a JSON file |
50+
51+
> 💡 If you wish to create multiple files for an entry, set the return value to an array of objects, each containing a `content`, `format` and `path` properties.
52+
53+
### 👀 Example configuration
54+
55+
_sourcebit.js_
56+
57+
```js
58+
module.exports = {
59+
plugins: [
60+
{
61+
module: require("sourcebit-target-hugo"),
62+
options: {
63+
writeFile: function(entry, utils) {
64+
const { __metadata: meta, ...fields } = entry;
65+
66+
if (!meta) return;
67+
68+
const { createdAt = "", modelName, projectId, source } = meta;
69+
70+
if (
71+
modelName === "post" &&
72+
projectId === "123456789" &&
73+
source === "sourcebit-source-contentful"
74+
) {
75+
const { __metadata, content, layout, ...frontmatterFields } = entry;
76+
77+
return {
78+
content: {
79+
body: fields["content"],
80+
frontmatter: { ...frontmatterFields, layout: fields["layout"] }
81+
},
82+
format: "frontmatter-md",
83+
path:
84+
"content/posts/" +
85+
createdAt.substring(0, 10) +
86+
"-" +
87+
utils.slugify(fields["title"]) +
88+
".md"
89+
};
90+
}
91+
}
92+
}
93+
}
94+
]
95+
};
96+
```
97+
98+
### 🧞‍♂️ Interactive setup process
99+
100+
This plugin offers an interactive setup process via the `npx create-sourcebit` command. It asks users to categorize each of the content models present in the `models` data bucket as a page or data object. For each model selected, the user is asked to define the location and the source of different frontmatter values.
101+
102+
## 📥 Input
103+
104+
This plugin expects the following data buckets to exist:
105+
106+
- `models`: An array of content models
107+
- `objects`: An array of content entries
108+
109+
## 📤 Output
110+
111+
This plugin creates files on disk, in locations and with formats defined by the `writeFile` function.

index.js

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
const inquirerTablePrompt = require("inquirer-table-prompt");
2+
const pkg = require("./package.json");
3+
const slugify = require("@sindresorhus/slugify");
4+
const { getSetupForData, getSetupForPage } = require("./lib/setup");
5+
6+
module.exports.name = pkg.name;
7+
8+
module.exports.transform = ({ data, log, options }) => {
9+
if (typeof options.writeFile !== "function") {
10+
return data;
11+
}
12+
13+
const utils = {
14+
slugify
15+
};
16+
const files = data.objects.reduce((result, object) => {
17+
let processedObject = object;
18+
19+
// Unless `options.fullAssetObjects` is true, we reduce any asset objects
20+
// down to a string containing just the URL.
21+
if (!options.fullAssetObjects) {
22+
processedObject = Object.keys(object).reduce((result, fieldName) => {
23+
const value =
24+
object[fieldName].__metadata &&
25+
object[fieldName].__metadata.modelName === "__asset"
26+
? object[fieldName].url
27+
: object[fieldName];
28+
29+
return {
30+
...result,
31+
[fieldName]: value
32+
};
33+
}, {});
34+
}
35+
36+
const writer = options.writeFile(processedObject, utils);
37+
38+
if (!writer) return result;
39+
40+
return result.concat(writer);
41+
}, []);
42+
43+
return {
44+
...data,
45+
files: (data.files || []).concat(files)
46+
};
47+
};
48+
49+
module.exports.getOptionsFromSetup = ({ answers, debug }) => {
50+
const { data: dataObjects = [], pages = [] } = answers;
51+
const conditions = [];
52+
53+
pages.forEach(page => {
54+
const { modelName, projectId, source } = page.__model;
55+
56+
let location = "";
57+
58+
if (page.location.fileName) {
59+
location = `'${page.location.fileName}'`;
60+
} else {
61+
const { directory, fileNameField, useDate } = page.location;
62+
const locationParts = [];
63+
64+
if (directory) {
65+
locationParts.push(`'${directory}/'`);
66+
}
67+
68+
if (useDate) {
69+
locationParts.push(`createdAt.substring(0, 10) + '-'`);
70+
}
71+
72+
locationParts.push(`utils.slugify(fields['${fileNameField}']) + '.md'`);
73+
location = locationParts.join(" + ");
74+
}
75+
76+
const contentField = page.contentField
77+
? `fields['${page.contentField}']`
78+
: "{}";
79+
const layout =
80+
page.layoutSource === "static"
81+
? `'${page.layout}'`
82+
: `fields['${page.layout}']`;
83+
const extractedProperties = [
84+
"__metadata",
85+
page.contentField ? `'${page.contentField}': content` : null,
86+
page.layoutSource ? "layout" : null,
87+
"...frontmatterFields"
88+
];
89+
90+
conditions.push(
91+
`if (modelName === '${modelName}' && projectId === '${projectId}' && source === '${source}') {`,
92+
` const { ${extractedProperties.filter(Boolean).join(", ")} } = entry;`,
93+
``,
94+
` return {`,
95+
` content: {`,
96+
` body: ${contentField},`,
97+
` frontmatter: ${
98+
page.layoutSource
99+
? `{ ...frontmatterFields, layout: ${layout} }`
100+
: "frontmatterFields"
101+
},`,
102+
` },`,
103+
` format: 'frontmatter-md',`,
104+
` path: ${location}`,
105+
` };`,
106+
`}\n`
107+
);
108+
});
109+
110+
dataObjects.forEach(dataObject => {
111+
const { modelName, projectId, source } = dataObject.__model;
112+
const { format, isMultiple } = dataObject;
113+
const location = dataObject.location.fileName
114+
? `'${dataObject.location.fileName}'`
115+
: `fields['${dataObject.location.fileNameField}']`;
116+
117+
conditions.push(
118+
`if (modelName === '${modelName}' && projectId === '${projectId}' && source === '${source}') {`,
119+
` const { __metadata, ...fields } = entry;`,
120+
``,
121+
` return {`,
122+
` append: ${isMultiple},`,
123+
` content: fields,`,
124+
` format: '${format}',`,
125+
` path: ${location}`,
126+
` };`,
127+
`}\n`
128+
);
129+
});
130+
131+
const functionBody = `
132+
// This function is invoked for each entry and its return value determines
133+
// whether the entry will be written to a file. When an object with \`content\`,
134+
// \`format\` and \`path\` properties is returned, a file will be written with
135+
// those parameters. If a falsy value is returned, no file will be created.
136+
const { __metadata: meta, ...fields } = entry;
137+
138+
if (!meta) return;
139+
140+
const { createdAt = '', modelName, projectId, source } = meta;
141+
142+
${conditions.join("\n")}
143+
`.trim();
144+
145+
debug("Function body: %s", functionBody);
146+
147+
return {
148+
writeFile: new Function("entry", "utils", functionBody)
149+
};
150+
};
151+
152+
module.exports.getSetup = ({ chalk, data, inquirer }) => {
153+
inquirer.registerPrompt("table", inquirerTablePrompt);
154+
155+
return async () => {
156+
const { models: modelTypes } = await inquirer.prompt([
157+
{
158+
type: "table",
159+
name: "models",
160+
message: "Choose a type for each of the following models:",
161+
pageSize: 7,
162+
rows: data.models.map((model, index) => ({
163+
name: `${model.modelLabel || model.modelName}\n${chalk.dim(
164+
`└${model.source}`
165+
)}`,
166+
value: index
167+
})),
168+
columns: [
169+
{
170+
name: "Page",
171+
value: "page"
172+
},
173+
{
174+
name: "Data",
175+
value: "data"
176+
},
177+
{
178+
name: "Skip",
179+
value: undefined
180+
}
181+
]
182+
}
183+
]);
184+
const dataModels = [];
185+
const pageModels = [];
186+
187+
modelTypes.forEach((type, index) => {
188+
if (type === "data") {
189+
dataModels.push(data.models[index]);
190+
} else if (type === "page") {
191+
pageModels.push(data.models[index]);
192+
}
193+
});
194+
195+
let queue = Promise.resolve({ data: [], pages: [] });
196+
197+
pageModels.forEach((model, index) => {
198+
queue = queue.then(async setupData => {
199+
console.log(
200+
`\nConfiguring page: ${chalk.bold(
201+
model.modelLabel || model.modelName
202+
)} ${chalk.reset.italic.green(
203+
`(${index + 1} of ${pageModels.length}`
204+
)})`
205+
);
206+
207+
return getSetupForPage({ chalk, data, inquirer, model, setupData });
208+
});
209+
});
210+
211+
dataModels.forEach((model, index) => {
212+
queue = queue.then(async setupData => {
213+
console.log(
214+
`\nConfiguring data object: ${chalk.bold(
215+
model.modelLabel || model.modelName
216+
)} ${chalk.reset.italic.green(
217+
`(${index + 1} of ${dataModels.length}`
218+
)})`
219+
);
220+
221+
return getSetupForData({ chalk, data, inquirer, model, setupData });
222+
});
223+
});
224+
225+
return queue;
226+
};
227+
};

0 commit comments

Comments
 (0)