Ember Times-Issue 49


Breaking news: just import from NPM! 🚨

Just before the editing deadline, we received reports from the Internet that there is now a way to easily npm install A kind package with import It enters your Ember application.Plug-in Manual automatic import
Will allow Zero configuration import Open the box today, it should be suitable until Packaging function Log in to Ember CLI.The report also claims that the plugin author and Ember Core team members @ef4 Means His work is deeply inspired Series of specific blog posts by using hashtags # EmberJS2018.

Sat Sri Akal Emberistas!

We have one this week 🌟 Special edition 🌟😲 to you: we will look at the internal structure of the new Ember Guides website, it has a Completely reinvented and relaunched this month It ended up running on the amazing Ember application. This will ultimately make the contribution of the Ember community very easy. ✨

In this special edition of Ember Times, @real_ate Who supports Guide migration Let us see New application Inside and into its broccoli powered by Build the pipeline. So get ready to reach your recommended daily vegetable intake πŸ₯’πŸ₯• and read People Blog Or in us Email newsletter Regarding what happened in Emberland this week…


This is the second part of a six-part series on how we rebuilt the new Ember guide from scratch in six months and converted it into an Ember app in the process.If you want to view the first part of this series, please check Here. You can track future posts in this series in the following ways RSS feed.

Preliminary experiment

In the early stages of the conversation about upgrading Ember Guides to the full-fledged Ember app, Ryan Tablada (aka @rtable) Led me to an experiment in which he had already started rolling the ball.It is called
Broccoli blog bee And aims to:

Convert the table of contents (or set of tables) of Markdown documents to static JSON:API.

Working extensively with Broccoli many years ago (before Ember CLI became Ember’s official build system), I thought to myself “What is the worst possible scenario?” and jumped directly into the code. The thing about broccoli is that it is almost the opposite of “biking”, if you haven’t used it for a while, you will soon forget everything about it…😣

Why we use Broccoli & JSON: API

Anyone who has followed Ember within a reasonable amount of time knows that Ember Data works well with JSON:API. If your backend is already using JSON:API and following the specification, then you are basically ready to start! If you ever needed to integrate a hand-made, customized API endpoint with Ember Data, you would know that it is essentially a process of converting content to Ember Data in JavaScript before converting the content to JSON:API. If you use JSON:API up front, things will be easier to handle, and you can take advantage of the simplicity of Ember Data.

Broccoli is a Asset pipeline This handles the file system very efficiently. In theory it is just Javascriptℒ️.One of the issues that makes Broccoli more challenging to use is the lack of documentation, or at least used That’s it. In the past few months, Ollie Griffith Has been very active in the broccoli community and recently released a Broccoli tutorialThere is still a lot of work going on behind the scenes to make Broccoli easier to use and a more powerful tool.For example, Oli currently Do an experiment Bringing Broccoli 1.x support to Ember CLI will (hopefully) make the lives of Windows developers better. Jane Weber Also working hard to update Ember CLI documentationSoon, it should be easier to start modifying the build pipeline in Ember CLI via Broccoli! πŸŽ‰

After making these initial decisions, we finally decided to establish a project called Broccoli static site json, As you can see, its goal is very similar broccoli-blog-api:

A simple Broccoli plug-in for parsing a collection of Markdown files and displaying them as JSON:API documents under the specified path in the output tree. It also supports the use of front-matter to define metadata for each Markdown file.

Since the early days broccoli-static-site-json, Things get a little more complicated…More flexibility usually means more complexity! But to understand the basics of Broccoli’s effectiveness in this use case, we can review the first submission on November 7, 2017.We will go into details below, but if you want to follow you can find the main index file Here.

Main plugin

Early experiments broccoli-static-site-json Have a index.js The file (the only active file at the time) has a total of 119 lines of code, and the main active line constitutes build() The Broccoli plug-in only adds up to 50 lines of code, which is absolutely small enough that we can delve into it in this article. πŸ’ͺ

I will briefly outline the structure of the Broccoli plug-in, and then detail each of the main lines build() Features.

Structure of the Broccoli plug-in

This is a basic example of a plugin:

const Plugin = require('broccoli-plugin');

class BroccoliStaticSiteJson extends Plugin {
  constructor(folder, options) {
    // tell broccoli which "nodes" we're watching
    super([folder], options);
    this.options = {
      folder,
      contentFolder: 'content',
      ...options,
    };
    // don't know what this does
    Plugin.call(this, [folder], {
      annotation: options.annotation,
    });
  }

  build() {}
}

module.exports = BroccoliStaticSiteJson;

This is not exactly most Basic example of the plug-in, because it has some business logic and API broccoli-static-site-json exposed. You can’t tell from the above example, but it tells us that if we want to use this plugin, we will do something like this:

const jsonTree = new StaticSiteJson('input', {
  contentFolder: 'output-jsons',
})

This is setting local folder with contentFolder In the option hash StaticSiteJson Class, will eventually help tell the plug-in to input Folder and put the output JSON:API file into output-jsons. This contentFolder Is optional and will default to content.

When used in Ember CLI or any other Broccoli pipeline, build() The function is called. This is where most of the work happens.

Build() function

Show everyone build() Function, and then break it down one by one. Note: I have deleted some things that do not need to explain this process, such as some optional defensive programming steps, to make it easier to understand.

build() {
  // build content folder if it doesn't exist
  if (!existsSync(join(this.outputPath, this.options.contentFolder))) {
    mkdirSync(join(this.outputPath, this.options.contentFolder));
  }

  // build pages file
  if (existsSync(join(this.options.folder, 'pages.yml'))) {
    let pages = yaml.safeLoad(readFileSync(join(this.options.folder, 'pages.yml'), 'utf8'));

    writeFileSync(join(this.outputPath, this.options.contentFolder, 'pages.json'), JSON.stringify(TableOfContentsSerializer.serialize(pages)));
  }

  // build the tree of MD files
  const paths = walkSync(this.inputPaths);

  const mdFiles = paths.filter(path => extname(path) === '.md');

  const fileData = mdFiles.map(path => ({
    path,
    content: readFileSync(join(this.options.folder, path)),
  })).map(file => ({
    path: file.path,
    ...yamlFront.loadFront(file.content),
  }));

  fileData.forEach((file) => {
    const directory = dirname(join(this.outputPath, this.options.contentFolder, file.path));
    if (!existsSync(directory)) {
      mkdirSync(dirname(join(this.outputPath, this.options.contentFolder, file.path)));
    }

    const serialized = ContentSerializer.serialize(file);

    writeFileSync(join(this.outputPath, this.options.contentFolder, `${file.path}.json`), JSON.stringify(serialized));
  });
}

This may seem a little scary, but don’t worry we will break it down, hope everything becomes clear!

Create output folder

The first part is some general cleaning. We want to make sure that the output folder exists before proceeding, if it does not exist, we need to create it:

// build content folder if it doesn't exist
if (!existsSync(join(this.outputPath, this.options.contentFolder))) {
  mkdirSync(join(this.outputPath, this.options.contentFolder));
}

One thing you will notice immediately is that we are using a similar feature exitsSync(), mkdirSync() with join() These are all native NodeJS functions.If you look at the top, you can see where they came from index.js File to view the require statement:

const { extname, join, dirname } = require('path');
const {
  readFileSync,
  writeFileSync,
  mkdirSync,
  existsSync,
} = require('fs');

You can read more about these functions in the official NodeJS documentation fs with path.

Create directory from page file

Before i start building broccoli-static-site-json, Ricardo Mendes aka @locks with Jared Galanis The process of building the Markdown source catalog has begun, which will allow us to manage different versions of the Ember guide more effectively.A key aspect of this structure is that it includes a pages.yml A file that specifies a table of contents (ToC) for any specific version of the guide. As part of this process, what we need to do is to parse the YAML file and output a JSON:API-based file in the output directory. This is the code:

// build pages file
if (existsSync(join(this.options.folder, 'pages.yml'))) {
  let pages = yaml.safeLoad(readFileSync(join(this.options.folder, 'pages.yml'), 'utf8'));

  writeFileSync(join(this.outputPath, this.options.contentFolder, 'pages.json'), JSON.stringify(TableOfContentsSerializer.serialize(pages)));
}

This code snippet first checks if the input folder contains pages.yml File if it loads it using js-yaml. After loading the data, it will write a Serialized The file version to the output folder, and use to complete the serialization jsonapi serializer Has the following serializer definition:

const TableOfContentsSerializer = new Serializer('page', {
  id: 'url',
  attributes: [
    'title',
    'pages',
  ],
  keyForAttribute: 'cammelcase',
});

Building the Markdown file tree

Next is the highlight, converting the nested structure of Markdown files into the nested structure of JSON:API documents. If we divide it into bite-sized pieces, this will be easier to understand. Let’s start by getting the Markdown file:

const paths = walkSync(this.inputPaths);

const mdFiles = paths.filter(path => extname(path) === '.md');

This code uses walkSync List all files under inputPaths (we act as folder In the constructor), and then we filter the list of paths to find all .mdSo that we can find the Markdown file.

Next is the time to load each file into the array:

const fileData = mdFiles.map(path => ({
  path,
  content: readFileSync(join(this.options.folder, path)),
})).map(file => ({
  path: file.path,
  ...yamlFront.loadFront(file.content),
}));

We used Array.map() List of converted files twice first name Into a data structure that contains everything we need. The first mapping converts the file name into an array of objects, as shown below:

[{
  path: '/getting-started/index.md',
  content: `---
            title: Getting Started
            ---
            Getting started with Ember is easy. Ember projects are created ...`
}, {
  path: '/getting-started/quick-start.md',
  content: `---
            title: Quick Start
            ---
            This guide will teach you how to build a simple ...`
}]

As you can see, each object remembers the path of the file created and has Full content Loaded file.In the second map() Functions we use yaml-front-matter Load optional additional YAML metadata into the object.You can read more about what a front end is and what it can be used for Here.

After the second time map() Features fileData The array looks like this:

[{
  path: '/getting-started/index.md',
  title: 'Getting Started',
  __content: 'Getting started with Ember is easy. Ember projects are created ...'
}, {
  path: '/getting-started/quick-start.md',
  title: 'Quick Start',
  __content: 'This guide will teach you how to build a simple ...'
}]

This makes us finally ready to serialize to JSON: API.Next we need to loop fileData Array and write our JSON file to disk:

fileData.forEach((file) => {
  const directory = dirname(join(this.outputPath, this.options.contentFolder, file.path));
  if (!existsSync(directory)) {
    mkdirSync(dirname(join(this.outputPath, this.options.contentFolder, file.path)));
  }

  const serialized = ContentSerializer.serialize(file);

  writeFileSync(join(this.outputPath, this.options.contentFolder, `${file.path}.json`), JSON.stringify(serialized));
});

The first thing we do in this function is to ensure that the folder where we want to write the file actually exists.We need to check all files because we use walkSync Early in the process, there may be a very deeply nested folder structure.

Next we serialize file Use another object jsonapi-serializer And write the serialized document to disk.This is the definition of the serializer ContentSerializer, This is just a little more complicated than the page in the ToC:

const ContentSerializer = new Serializer('content', {
  id: 'path',
  attributes: [
    '__content',
    'title',
  ],
  keyForAttribute(attr) {
    switch (attr) {
      case '__content':
        return 'content';
      default:
        return attr;
    }
  },
});

In this case, we use keyForAttribute() Rename __content become content.

in conclusion

I hope you like to learn more broccoli-static-site-json. If you are interested in other places where the system is used, you can check Ember Casper template, Which also happens to be motivation Stone Circle Blog. πŸŽ‰

As always, you can contact me in the following ways Twitter, Or you can find me on Slack in the Ember community as @real_ate.


We would like to thank everyone who made the new guide app possible!Praise Mansona, Sivakumar-Kailasam, Jan Weber, rwwagner90, Chris Mu, native, with, with Doping -We thank you for all your efforts to update one of our favorite documentation sites! πŸ’–


That is another package! ✨

Be kind,

Chris Manson, Sivakumar Kailasam, Amy Lam, Ryan Mark, Jessica Jordan and the learning team



Leave a Reply

Your email address will not be published. Required fields are marked *