npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@thereforejs/nutrition

v0.0.1

Published

Nutrition declaration from recipes under the EU food information regulation (Regulation (EU) No 1169/2011, known as FIC, LMIV or INCO)

Readme

[!WARNING] Early development (0.x). The API may still change. The library is no legal advice: the label stays the responsibility of the food business.

Install

npm install @thereforejs/nutrition

At a glance

Bread rolls: the recipe in grams as it comes out of the oven, values from @thereforejs/foods. The baking loss is taken off the water.

import { calculateNutrition } from "@thereforejs/nutrition";
import { de, en } from "@thereforejs/nutrition/locales";
import { C214200, N110000, R111000, R459000 } from "@thereforejs/foods";

const { per100g, perPiece, report } = calculateNutrition(
  {
    ingredients: [
      { amount: 1000, per100g: C214200.per100g }, // wheat flour type 550
      { amount: 420, per100g: N110000.per100g }, // water: 600 g less 180 g baking loss
      { amount: 40, per100g: R459000.per100g }, // fresh yeast
      { amount: 20, per100g: R111000.per100g }, // salt
    ],
    pieceWeight: 60,
  },
  { locales: [de, en] },
);

The declaration, each value as text in every locale you import:

| | pro 100 g | pro Brötchen (60 g) | | ---------------------------- | ------------------ | ------------------- | | Energie | 1016 kJ / 240 kcal | 609 kJ / 144 kcal | | Fett | 0,8 g | <0,5 g | | davon: gesättigte Fettsäuren | <0,1 g | <0,1 g | | Kohlenhydrate | 48 g | 29 g | | davon: Zucker | <0,5 g | <0,5 g | | Eiweiß | 8,5 g | 5,1 g | | Salz | 1,3 g | 0,78 g |

| | per 100 g | per roll of 60 g | | ------------------ | ------------------ | ----------------- | | energy | 1016 kJ / 240 kcal | 609 kJ / 144 kcal | | fat | 0.8 g | <0.5 g | | of which saturates | <0.1 g | <0.1 g | | carbohydrate | 48 g | 29 g | | of which sugars | <0.5 g | <0.5 g | | protein | 8.5 g | 5.1 g | | salt | 1.3 g | 0.78 g |

Every value also comes as data: exact, rounded, negligible, atMost. What the calculation went on without is in report: the BLS has no value for polyols of fresh yeast, so the energy is calculated without them and the report says so.

More recipes from easy to hard (voluntary nutrients, % reference intake, a supplier's sheet, a refused input) are in examples/recipes.ts: vp run example.

Checked against published declarations (tests/oracle.test.ts): the EuroFIR recipe example, and the worked examples of two labelling guides. Same sums, same rounding; the energy differs only where a partner calculates it from the rounded values (we use the exact ones, SPEC 10).

Usage

import { calculateNutrition } from "@thereforejs/nutrition";
import { de } from "@thereforejs/nutrition/locales";

const { per100g, perPiece, report } = calculateNutrition(
  {
    ingredients: [
      {
        amount: 1000,
        per100g: { fat: 1, saturates: 0.2, carbohydrate: 70, sugars: 1, protein: 10, fibre: 4 },
      },
      { amount: 980, per100g: {} }, // water
      { amount: 20, per100g: { salt: 100 } },
    ],
    pieceWeight: 50, // optional
  },
  { locales: [de], voluntary: ["fibre"] },
);

per100g.energy.kJ; // { exact: "714.5", rounded: "715", negligible: false, atMost: false, text: { "de-DE": "715 kJ" } }
per100g.fat; //       { exact: "0.5", rounded: "0.5", negligible: true, atMost: true, text: { "de-DE": "<0,5 g" } }

Amounts are grams as they end up in the product; their sum is the product weight. Values are grams per 100 g, as a number or decimal text. Energy is always calculated, never passed.

| Option | Default | Meaning | | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | locales | [] | imported locales, each adds its text; import only what you need | | negligible | "auto" | a negligible amount (Table 4): "auto" reads "0 g" when there is none and "<0,5 g" (atMost: true) when there is some; "lessThan" always "<0,5 g", "zero" always "0 g" | | voluntary | [] | voluntary nutrients to declare: monounsaturates, polyunsaturates, polyols, starch, fibre | | referenceIntake | false | adds referenceIntake: { exact, rounded, text } (percent, whole, half up) to energy and the nutrients of Annex XIII Part B, and referenceIntakeStatement per locale (Art. 32(5)) |

Data

Values of over 7,100 foods come with @thereforejs/foods, and @thereforejs/food-finder finds them by what people type. Both are optional:

npm install @thereforejs/foods @thereforejs/food-finder
import { calculateNutrition } from "@thereforejs/nutrition";
import { C131000 } from "@thereforejs/foods"; // Oat whole grain, raw

calculateNutrition({ ingredients: [{ amount: 100, per100g: C131000.per100g }] });

A null in foods is unknown here and lands in the report; its marks say whether it is below a limit of detection.

Your values, your responsibility

The library knows no foods, only the values you pass. It checks that they are possible, not that they are right. What a value means is decided by what you send:

| You send | Meaning | The library | | -------- | ------------- | ---------------------------------------------- | | 0 | there is none | calculates with 0 | | no key | nothing said | calculates with 0 | | null | unknown | calculates without it and lists it in report |

Report

Everything the calculation went on without, grouped by code and nutrient, so it reads "2 ingredients without fibre". ingredients are positions in your input list.

report: [{ code: "value.missing", nutrient: "fibre", ingredients: [3, 7] }];

| Code | Meaning | What you can do | | --------------- | ---------------------------------------------- | -------------------------------------------------------------------------------- | | value.missing | these ingredients have null for the nutrient | find the value (supplier, another database, a lab), or send 0 if there is none |

Errors

Errors refuse the whole result and come as a DiagnosticError whose diagnostics carry a stable code, the path into your input and typed params; checking code narrows params.

try {
  calculateNutrition(input);
} catch (error) {
  if (error instanceof DiagnosticError) {
    for (const d of error.diagnostics) {
      if (d.code === "value.unknownKey") console.log(d.path, d.params.key);
    }
  }
}

| Code | Params | When | | ------------------------ | ---------------- | --------------------------------------------------------- | | input.invalid | | the input is not { ingredients: [{ amount, per100g }] } | | options.invalid | value | an option has a value it cannot have | | value.invalid | value | not a number or decimal text of at least 0 | | value.unknownKey | key | a key in per100g that is no nutrient (a typo, energy) | | value.above100 | | more than 100 g in 100 g (salt: more than 250 g) | | value.partExceedsWhole | part, whole | e.g. sugars above carbohydrate (Annex XV) | | salt.sodiumMismatch | salt, sodium | both given and salt is not sodium × 2.5 (Annex I No 11) | | weight.zero | | the product weighs 0 g | | pieceWeight.zero | | a piece of 0 g |

License

MIT