npm registry proxy with on-the-fly filtering

Overview

npm-registry-firewall    📦 📦 🔥

npm registry proxy with on-the-fly filtering

CI Maintainability Test Coverage npm (tag)

Key Features

  • Restricts access to remote packages by predicate:
  • Flexible configuration: use presets, plugins and define as many server/context-path/rules combinations as you need.
  • Extendable. expressjs-inspired server implementation is under the hood.
  • Standalone. No clouds, no subscriptions.
  • Linux / Windows / macOS compatible.
  • Has no deps. Literally zero.

Motivation

To mitigate security and legal risks

Open Source is essential for modern software development. According to various estimates, at least 60% of the resulting codebase is composed of open repositories, libraries and packages. And it keeps growing. Synopsys OSSRA 2021 report found that 98% of applications have open source dependencies.

But open does not mean free. The price is the risk that you take:

  • Availability
  • Security
  • Legality / license

Let's consider these problems in the context of the JS universe.

Availability risks

JS packages are distributed in various ways: git repos, cdns and package registries. Regardless of the method, there are only two entry types that are finally resolved by any pkg manager: git-commit pointers and tarball links.

"dependencies": {
  "yaf" : "git://github.com/antongolub/yarn-audit-fix.git#commit-hash",
  "yaf2": "antongolub/yarn-audit-fix",
  "yarn-audit-fix" : "*"
}
yaf2@antongolub/yarn-audit-fix:
  version "9.2.1"
  resolved "https://codeload.github.com/antongolub/yarn-audit-fix/tar.gz/706646bab3b4c7209596080127d90eab9a966be2"
  dependencies:
    "@types/find-cache-dir" "^3.2.1"
    "@types/fs-extra" "^9.0.13"
"node_modules/yaf": {
  "name": "yarn-audit-fix",
  "version": "9.2.1",
  "resolved": "git+ssh://[email protected]/antongolub/yarn-audit-fix.git#706646bab3b4c7209596080127d90eab9a966be2",
  "license": "MIT",
"node_modules/yarn-audit-fix": {
  "version": "9.2.1",
  "resolved": "https://registry.npmjs.org/yarn-audit-fix/-/yarn-audit-fix-9.2.1.tgz",
  "integrity": "sha512-4biFNP4ZLOHboB2cNVuhYyelTFR/twlfmGMQ2TgJgGRORMDM/rQdQqhJdVLuKvfdMLFEPJ832z6Ws5OoCnFcfA==",
  "dependencies": {

So the implementation of mirroring is fundamentally quite simple: we just need to save and expose these assets from an alternative ssh/https entry point. Luckily this has already happened. The main repository for JS code is registry.npmjs.org. And at least 5 public replicas are always available as alternatives:

If this reliability level is not enough, you can easily run one more registry:

Security risks

Any code may not work properly. Due to error or malice. Keep in mind that most OSS licenses exclude any liability for damages. It's also important to always remember that oss code is not verified before being published. These two circumstances sometimes give rise to dangerous incidents like colors.js or node-ipc.

The independent audit process is expensive, time consuming, so only setting a delay before using new pkg version might be effective countermeasure.

Legal risks

License agreement is an attribute of the moment: it can suddenly change and affect the development process (for example, husky-5). Uncontrolled use of new versions may have legal and financial consequences. Therefore, automated license checks should be part of CI/CD pipeline or the registry's own feature.

Implementation notes

The proxy intercepts packuments and tarball requests and applies the specified filters to them:

Requirements

Node.js >= 14

Install

# npm
npm i npm-registry-firewall

# yarn
yarn add npm-registry-firewall

Usage

CLI

npm-registry-firewall /path/to/config.json

JS API

import {createApp} from 'npm-registry-firewall'

const app = createApp({
  server: {
    host: 'localhost',
    port: 3001,
  },
  firewall: {
    registry: 'https://registry.npmmirror.com',
    rules: [
      {
        policy: 'allow',
        org: '@qiwi'
      },
      {
        policy: 'deny',
        name: '@babel/*,react@^17'  // All @babel-scoped pkgs and react >= 17.0.0
      },
      {
        policy: 'allow',
        filter: ({name, org}) => org === '@types' || name === 'react'  // may be async
      },
      {
        plugin: [['npm-registry-firewall/audit', {
          critical: 'deny',
          moderate: 'warn'
        }]]
      },
    ]
  }
})

await app.start()

TS libdefs

Included
type LetAsync<T> = T | Promise<T>

type TApp = {
  start: () => Promise<void>
  stop: () => Promise<void>
}

type TLogger = typeof console

type TServerConfig = {
  host?: string
  port?: string | number
  base?: string
  healthcheck?: string | null
  metrics?: string | null
  secure?: {
    key: string,
    cert: string
  }
  requestTimeout?: number
  headersTimeout?: number
  keepAliveTimeout?: number
  extend?: string
}

type TPolicy = 'allow' | 'deny' | 'warn'

type TRule = {
  policy?: TPolicy
  name?: string | RegExp | Array<string | RegExp>
  org?: string | RegExp | Array<string | RegExp>
  dateRange?: [string, string]
  age?: number | [number] | [number, number]
  version?: string,
  license?: string | RegExp | Array<string | RegExp>
  username?: string | RegExp | Array<string | RegExp>
  filter?: (entry: Record<string, any>) => LetAsync<boolean | undefined | null>
  extend?: string
  plugin?: TPluginConfig
}

type TPluginConfig = string | [string, any] | TPlugin | [TPlugin, any]

type TCacheConfig = {
  ttl: number
  evictionTimeout?: number
  name?: string
}

type TCacheImpl = {
  add(key: string, value: any, ttl?: number): LetAsync<any>
  has(key: string): LetAsync<boolean>
  get(key: string): LetAsync<any>
  del(key: string): LetAsync<void>
}

type TCacheFactory = {
  (opts: TCacheConfig): TCacheImpl
}

type TFirewallConfig = {
  registry: string
  entrypoint?: string
  token?: string
  base?: string
  rules?: TRule | TRule[]
  cache?: TCacheConfig | TCacheImpl | TCacheFactory
  extend?: string
}

type TConfig = {
  server: TServerConfig | TServerConfig[]
  firewall: TFirewallConfig
  extend?: string
}

type TValidationContext = {
  options: any,
  rule: TRule,
  entry: Record<string, any>
  boundContext: {
    logger: TLogger
    registry: string
    authorization?: string
    entrypoint: string
    name: string
    org?: string
    version?: string
  }
}

type TPlugin = {
  (context: TValidationContext): LetAsync<TPolicy>
}

type TAppOpts = {
  logger?: TLogger
  cache?: TCacheFactory
}

export function createApp(config: string | TConfig | TConfig[], opts?: TAppOpts): Promise<TApp>

export function createLogger(
  extra?: Record<string, any>, 
  formatter?: (logCtx: {level: string, msgChunks: string[], extra: Record<string, any>}) => void
): string

Config

{
  "server": {
    "host": "localhost",        // Defaults to 127.0.0.1
    "port": 3000,               // 8080 by default
    "secure": {                 // Optional. If declared serves via https
      "cert": "ssl/cert.pem",
      "key": "ssl/key.pem"
    },
    "base": "/",                // Optional. Defaults to '/'
    "healthcheck": "/health",   // Optional. Defaults to '/healthcheck'. Pass null to disable
    "metrics": "/metrics",      // Optional. Uptime, CPU and memory usage. Defaults to '/metrics'. null to disable
    "keepAliveTimeout": 15000,  // Optional. Defaults to 61000
    "headersTimeout": 20000,    // Optional. Defaults to 62000
    "requestTimeout": 10000     // Optional. Defaults to 30000
  },
  "firewall": {
    "registry": "https://registry.npmmirror.com",  // Remote registry
    "token": "NpmToken.*********-e0b2a8e5****",    // Optional bearer token
    "entrypoint": "https://r.qiwi.com/npm",        // Optional. Defaults to `${server.secure ? 'https' : 'http'}://${server.host}:${server.port}${route.base}`
    "base": "/",                // Optional. Defaults to '/'
    "cache": {                  // Optional. Defaults to no-cache (null)
      "ttl": 5,                 // Time to live in minutes. Specifies how long resolved pkg directives will live.
      "evictionTimeout": 1      // Cache invalidation period in minutes. Defaults to cache.ttl.
    },
    "extends": "@qiwi/internal-npm-registry-firewall-rules",  // Optional. Populates the entry with the specified source contents (json/CJS module only)
    "rules": [
      {
        "policy": "allow",
        "org": "@qiwi"
      },
      {
        "policy": "allow",
        "name": ["@babel/*", "@jest/*", "lodash"] // string[] or "comma,separated,list". * works as .+ in regexp
      },
      {
        "policy": "warn",       // `warn` directive works like `allow`, but also logs if someone has requested a tarball matching the rule
        "name": "reqresnext"
      },
      {
        "policy": "deny",
        "extends": "@qiwi/nrf-rule",  // `extends` may be applied at any level, and should return a valid value for the current config section
      },
      {
        "plugin": ["npm-registry-firewall/audit", {"moderate": "warn", "critical": "deny"}]
      },
      {
        "policy": "deny",
        "name": "colors",
        "version": ">= v1.4.0"  // Any semver range: https://github.com/npm/node-semver#ranges
      },
      {
        "policy": "deny",
        "license": "dbad"       // Comma-separated license types or string[]
      },
      {
        "policy": "allow",
        "username": ["sindresorhus", "isaacs"] // Trusted npm authors.
      },
      {
        "policy": "allow",
        "name": "d",
        // `allow` is upper, so it protects `< 1.0.0`-ranged versions that might be omitted on next steps
        "version": "< 1.0.0"
      },
      {
        "policy": "deny",
        // Checks pkg version publish date against the range
        "dateRange": ["2010-01-01T00:00:00.000Z", "2025-01-01T00:00:00.000Z"]
      },
      {
        "policy": "allow",
        "age": 5    // Check the package version is older than 5 days. Like quarantine
      }
    ]
  }
}

Multi-config

// Array at the top level
[
  // Two servers (for example, http and https) share the same preset
  {
    "server": [
      {"port": 3001},
      {"port": 3002, "secure": {"cert": "ssl/cert.pem", "key": "ssl/key.pem" }},
    ],
    "firewall": {
      "registry": "https://registry.yarnpkg.com",
      "rules": {"policy": "deny", "org": "@qiwi"}
    }
  },
  // One server has a pair of separately configured endpoints
  {
    "server": {"port": 3003},
    "firewall": [
      {"base": "/foo", "registry": "https://registry.npmjs.org", "rules": {"policy": "deny", "org": "@qiwi"}},
      {"base": "/bar", "registry": "https://registry.yarnpkg.com", "rules": {"policy": "deny", "org": "@babel"}}
    ]
  }
]

️More config examples

Cache

By default, nrf uses a simple in-memory cache to store patched packuments.

cache: {              // Optional. Defaults to no-cache (null)
  ttl: 5,             // Time to live in minutes. Specifies how long resolved pkg directives will live.
  evictionTimeout: 1, // Cache invalidation period in minutes. Defaults to cache.ttl.
  name: 'unique'      // If and only if you use the same rules for several firewall entrypoints (multi-port proxy)
                      // you can slighly optimise resource consupmtion by sharing the cache. Defaults to `randId()`
}

You can also provide your own implementation instead, for example, to create cassandra-based distributed cache:

import {createApp} from 'npm-registry-firewall'

const cache = {
  add() {}, // each method may be async
  has() {return false},
  get() {},
  del() {}
}

const app = createApp({
  server: {port: 5000},
  firewall: {
    registry: 'https://registry.npmjs.org',
    cache,
    rules: []
  }
})

Or even a cache factory:

const cache = () => {
  // ... init
  return {
    get() {},
    ...
  }
}

Pass null as config.firewall.cache to disable.

Extras

Presets

Introduce your own reusable snippets via extends or preset. This statement can be applied at any config level and should return a valid value for the current section. The specified path will be loaded synchronously through require, so it must be a JSON or CJS module.

const config = {
  // should return `firewall` and `servers`
  extends: '@qiwi/nrf-std-config',
  server: {
    port: 5000,
    extends: '@qiwi/nrf-server-config'
  },
  firewall: {
    // `rules`, `registry`, etc,
    extends: '@qiwi/nrf-firewall-config',
    // NOTE If you redefine `rules` the result will be contatenation of `[...rules, ...extends.rules]`
    rules: [{
      policy: 'deny',
      // `name`, `org`, `filter`, etc
      extends: '@qiwi/nrf-deprecated-pkg-list'
    }, {
      policy: 'allow',
      extends: '@qiwi/nrf-whitelisted-orgs'
    }, {
      extends: '@qiwi/nrf-all-in-one-filter'
    }]
  }
}

For example, extends as a filter:

// '@qiwi/nrf-all-in-one-filter'
module.exports = {
  filter({org, name, time, ...restPkgData}) {
    if (name === 'react') {
      return true
    }

    if (org === '@babel') {
      return false
    }

    if (restPkgData.license === 'dbad') {
      return false
    }
  }
}

Plugins

Plugin is slightly different from preset:

  • Async. It's loaded dynamically as a part of rule processing pipeline, so it may be an ESM.
  • Configurable. Opts may be passed as the 2nd tuple arg.
  • Composable. There may be more than one per rule.
const rule1 = {
  plugin: ['@qiwi/nrf-plugin']
}

const rule2 = {
  plugin: [
    ['@qiwi/nrf-plugin', {foo: 'bar'}],
    '@qiwi/nrf-another-one'
  ]
}

Plugin interface is an (async) function that accepts TValidationContext and returns policy type value or false as a result:

const plugin = ({
  rule,
  entry,
  options,
  boundContext
}) => entry.name === options.name ? 'deny' : 'allow'

npm-registry-firewall/audit

Some registries do not provide audit API, that's why the plugin is disabled by default. To activate, add a rule:

{
  plugin: [['npm-registry-firewall/audit', {
    critical: 'deny',
    moderate: 'warn'
  }]]
}

Monitoring

/healthcheck

{"status":"OK"}

/metrics

{
  "uptime": "00:00:47",
  "memory": {
    "rss": 34320384,
    "heapTotal": 6979584,
    "heapUsed": 5632224,
    "external": 855222,
    "arrayBuffers": 24758
  },
  "cpu": {
    "user": 206715,
    "system": 51532
  }
}

stdout

{"level":"INFO","timestamp":"2022-04-11T20:56:47.031Z","message":"npm-registry-firewall is ready for connections: https://localhost:3000"}
{"level":"INFO","timestamp":"2022-04-11T20:56:49.568Z","traceId":"44f21c050d8c6","clientIp":"127.0.0.1","message":"GET /d"}
{"level":"INFO","timestamp":"2022-04-11T20:56:50.015Z","traceId":"44f21c050d8c6","clientIp":"127.0.0.1","message":"HTTP 200 446ms"}

logger

You can override the default implementation if needed:

import { createLogger, createApp } from 'npm-registry-firewall'

const logger = createLogger(
  {foo: 'bar'}, // extra to mix
  ({level, msgChunks, extra}) => JSON.stringify({
    msg: msgChunks.map(m => '' + m),
    mdc_trace: {spanId: extra.traceId, traceId: extra.traceId, bar: extra.foo},
    timestamp: new Date().toISOString(),
    level
  })
)

const app = createApp(cfg, {logger})

Manual testing

.npmrc

registry=https://localhost:3000
strict-ssl=false

run

# node src/main/js/cli.js config.json
yarn start 

npm view

npm-registry-firewall % npm view d versions                          
[ '0.1.0', '0.1.1' ]

curl

curl -k  https://localhost:3000/registry/minimist/-/minimist-1.2.6.tgz > minimist.tgz
curl -k  https://localhost:3000/registry/react > react.json

Contributing

Feel free to open any issues: bug reports, feature requests or questions. You're always welcome to suggest a PR. Just fork this repo, write some code, put some tests and push your changes. Any feedback is appreciated.

License

MIT

Comments
  • v2

    v2

    closes #75

    • make cache singleton
    • introduce fallback firewall profile
    • simptify config
    • [x] New code is covered by tests
    • [x] All the changes are mentioned in docs (readme.md)
    enhancement released refactoring 
    opened by antongolub 2
  • fix: handle url-encoded slashes case-insensitively

    fix: handle url-encoded slashes case-insensitively

    Hi,

    package names that have URL-encoded slash in uppecase (i.e. @babel%2Fcore) are not processed by middlewares because they don't match the regex. I fixed the regexes.

    • [ ] New code is covered by tests
    • [ ] All the changes are mentioned in docs (readme.md)
    released 
    opened by ilyakhokhryakov 2
  • feat: simplify config format

    feat: simplify config format

    before:

    {
      server: {port: 5000},
      agent: {
        keepAliveMsecs: 5000,
        keepAlive: true,
        maxSockets: 10_000,
        timeout: 10_000
      },
      firewall: [{
        base: '/foo',
        registry: 'https://registry.npmjs.org',
        rules: [],
        cache: {
          ttl: 5,
          evictionTimeout: 1,
          limit: 1_000_000
        }
      }, {
        base: '/bar',
        registry: 'https://registry.yarnpkg.com/',
      }]
    }
    

    after:

    {
      server: {port: 5000},
      agent: {
        keepAliveMsecs: 5000,
      },
      cache: {
        ttl: 5,
        evictionTimeout: 1,
        limit: 1_000_000
      },
      firewall: {
        '/foo': {
          registry: 'https://registry.npmjs.org',
          rules: [],
        },
        '/bar': {
          registry: 'https://registry.yarnpkg.com/',
        }
      }
    }
    
    enhancement released 
    opened by antongolub 1
  • feat: introduce smth like `warmup` for plugins

    feat: introduce smth like `warmup` for plugins

    Issue type

    • [ ] bug report
    • [x] idea
    • [ ] question
    • [ ] other
    const pipeline = resolve(...)
    pipeline.forEach(({warmup}) => warmup?.({name, registry}))
    
    enhancement released 
    opened by antongolub 1
  • feat: support several target registries

    feat: support several target registries

    closes #62

    Changes

    {
      server: { port: 3003 },
      firewall: {
        registry: ['https://registry.yarnpkg.com', 'https://registry.npmjs.org'],
        rules: { policy: 'deny', name: '*' }
      }
    }
    
    • [x] New code is covered by tests
    • [x] All the changes are mentioned in docs (readme.md)
    released 
    opened by antongolub 1
  • feat: support several target registries

    feat: support several target registries

    Issue type

    • [ ] bug report
    • [x] idea
    • [ ] question
    • [ ] other
    const app = createApp({
      server: {port: 5000},
      firewall: {
        registry: ['https://registry.npmjs.org', 'https://registry.yarnpkg.com'],
        rules: [...]
      }
    })
    
    enhancement released 
    opened by antongolub 1
  • feat: let http agent opts be configurable

    feat: let http agent opts be configurable

    Issue type

    • [ ] bug report
    • [x] idea
    • [ ] question
    • [ ] other

    Expected behavior

    {
      "server": {},
      "agent": {
        "keepAliveMsecs": 5000,
        "keepAlive": true,
        "maxSockets": 10000,
        "timeout": 10000
      }
    }
    
    enhancement released 
    opened by antongolub 1
  • fix: exclude beta-versions from distTags.latest

    fix: exclude beta-versions from distTags.latest

    Issue type

    • [x] bug report
    • [ ] idea
    • [ ] question
    • [ ] other

    Expected behavior

    versions = omitBeta(versions)
    

    Actual behavior

    const latestVersion = Object.keys(versions).reduce((m, v) => time[m] > time[v] ? m : v , null);
      const distTags = { latest: latestVersion }
    
    bug released 
    opened by antongolub 1
  • wip: add vulners plugin

    wip: add vulners plugin

    Resolves #43

    Changes

    {
      plugin: [['npm-registry-firewall/vulners', [{
          relevance: 95,  // Optional. Lower bound on the search result (lucene score 0...100) for associating the found CVEs with the target package. Defautls to 90
          cond: 'or',     // Optional. Defaults to `and`
          policy: 'warn',
          'cvss.score': 60,
          'cvss3.cvssV3.availabilityImpact': 'MEDIUM'
        }, {
          policy: 'deny',
          'cvss.score': 95,
          'cvss3.cvssV3.availabilityImpact': 'CRITICAL'
        }],
      ]]
    }
    
    • [ ] New code is covered by tests
    • [ ] All the changes are mentioned in docs (readme.md)
    opened by antongolub 0
  • feat: provide reversed mapping for audit plugin

    feat: provide reversed mapping for audit plugin

    Issue type

    • [ ] bug report
    • [x] idea
    • [ ] question
    • [ ] other

    Suggested behavior

    {
      plugin: [['npm-registry-firewall/audit', {
        deny: 'critical',
        warn: 'moderate'
      }]]
    }
    

    Actual behavior

    {
      plugin: [['npm-registry-firewall/audit', {
        critical: 'deny',
        moderate: 'warn'
      }]]
    }
    
    opened by antongolub 0
Releases(v2.10.1)
Owner
Anton Golub
Principal Engineer at @qiwi / UTC+3
Anton Golub
npm i uuid, npm i nodemon, npm i commander

goit-nodejs-hw-01 Получаем и выводим весь список контактов в виде таблицы (console.table) node index.js --action list Получаем контакт по id node inde

Oksana Banshchykova 3 Jul 5, 2022
This experimental library patches the global custom elements registry to allow re-defining or reload a custom element.

Redefine Custom Elements This experimental library patches the global custom elements registry to allow re-defining a custom element. Based on the spe

Caridy Patiño 21 Dec 11, 2022
Cosmos chain registry ⚛️

chain-registry The npm package for the Official Cosmos chain registry npm install chain-registry example import { assets, chains, ibc } from 'chain-r

Cosmology 19 Dec 8, 2022
This is service registry server based on Spring Cloud Config, Netflix and Eureka

Servicec Discovery and Registry This is service registry, based on Spring Cloud Netflix, Eureka and Spring Cloud Config. Full documentation is availab

Mohammad Nuruzzaman 1 Oct 11, 2022
proxy 🦄 yxorp is your Web Proxy as a Service (SAAS) Multi-tenant, Multi-Threaded, with Cache & Article Spinner

proxy ?? yxorp is your Web Proxy as a Service (SAAS) Multi-tenant, Multi-Threaded, with Cache & Article Spinner. Batteries are included, Content Spinning and Caching Engine, all housed within a stunning web GUI. A unique high-performance, plug-and-play, multi-threaded website mirror and article spinner

4D/ҵ.com Dashboards 13 Dec 30, 2022
Proxy but misspelled -- closed proxy for the internet

pyrox Proxy that runs on Cloudflare Workers. Setup Install wrangler2. npm install wrangler. Generate a public Ed25519 key, exported under SPKI mode wi

bots.gg 10 Sep 9, 2022
Fancytree - JavaScript tree view / tree grid plugin with support for keyboard, inline editing, filtering, checkboxes, drag'n'drop, and lazy loading

Fancytree Fancytree (sequel of DynaTree 1.x) is a JavaScript tree view / tree grid plugin with support for keyboard, inline editing, filtering, checkb

Martin Wendt 2.6k Jan 9, 2023
Fallback for SVG images by automatically creating PNG versions on-the-fly

SVGMagic - Cross browser SVG This repository is no longer actively mainted. It has proven to be very usefull back in 2013, but these days SVGs are sup

Dirk Groenen 596 Jul 27, 2022
Extract data-like things from a website on the fly.

There-should-be-an-API Oh, I think this website needs an API. Extract data-like things from a website on the fly. Demo The demo API is hosted on a 256

JacobLinCool 3 Mar 26, 2022
Asciifly is a webapp where you can asciify images and youtube videos on the fly.

Asciifly Asciifly is a webapp where you can asciify images and youtube videos on the fly. Come visit at https://asciifly.com Hosting I'm hosting this

André Esser 7 May 23, 2022
The Remix Stack for deploying to Fly with Supabase, authentication, testing, linting, formatting, etc.

Remix Supa Fly Stack Learn more about Remix Stacks. npx create-remix --template rphlmr/supa-fly-stack What's in the stack Fly app deployment with Doc

Raphaël Moreau 157 Jan 7, 2023
The Remix Blog Stack for deploying to Fly with MDX, SQLite, testing, linting, formatting, etc.

Remix Speed Metal Stack Learn more about Remix Stacks. npx create-remix --template Girish21/speed-metal-stack Remix Blog ?? This blog starter template

Girish 141 Jan 2, 2023
The Remix Stack for deploying to Fly with SQLite, authentication, testing, linting, formatting, etc.

Remix Indie Stack Learn more about Remix Stacks. npx create-remix --template remix-run/indie-stack What's in the stack Fly app deployment with Docker

Remix 688 Dec 30, 2022
The Remix Stack for deploying to Fly with PostgreSQL, authentication, testing, linting, formatting, etc.

The Remix Stack for deploying to Fly with PostgreSQL, authentication, testing, linting, formatting, etc.

Remix 677 Jan 2, 2023
Generate fluid, custom property based design systems on the fly — entirely based on Utopia

Fluid Design Systems With Netlify On-demand Builders A proof of concept demonstrating how Netlify on-demand builders can help generate fluid, custom p

George Francis 53 Jan 5, 2023
Remix+EdgeDB+Tailwind+Fly.io=🖤

Remix Chop Suey Stack Forked from Supa Fly Stack. Learn more about Remix Stacks. Quickstart npx create-remix --template jkcorrea/remix-chop-suey-stack

Jake Correa 56 Dec 22, 2022
Deno app to serve gmi pages on-the-fly for a gemini instance of my portfolio & blog

Aries Deno app to serve gmi pages on-the-fly for a gemini instance of my portfolio & blog Usage In order to run locally, you'll need SSL certs. You ca

Maxim 2 Jun 13, 2022
Transpile TypeScript on the fly and deliver it from your server as ES Modules.

ts-serve TypeScript + ES Modules Transpile TypeScript on the fly and serve it from your server as ES Modules. import { serve } from "https://deno.land

ayame113 28 Aug 15, 2022