Coder Social home page Coder Social logo

ggoodman / velcro Goto Github PK

View Code? Open in Web Editor NEW
190.0 7.0 9.0 54.99 MB

A set of tools and libraries for stitching together modules and code in highly dynamic browser environments

Home Page: https://ggoodman.github.io/velcro/

License: MIT License

JavaScript 10.10% TypeScript 88.95% HTML 0.94%
bundler npm browser

velcro's Introduction

Velcro

Velcro is a suite of packages designed to allow resolving modules in any JavaScript context, from any source. Velcro provides to tools to build a graph of these modules and then flatten this graph into bundles or executable code.

โœ… Why you might be interested in Velcro

Beyond beeing intrinsically interesting for the challenges it faces and the approaches taken to address these, there are a number of reasons why you might be interested in Velcro.

  1. You would like to run code in the browser but you can't predict the structure, or dependencies of that code. Velcro can help combine dynamic code with NPM modules coming from a CDN like unpkg.com or jsDelivr.com.
  2. You would like to bundle some code with NPM dependencies but do not have access to a filesystem or do not want to run npm install.
  3. You would like to resolve modules and read their content from a CDN like unpkg.com or jsDelivr.com in a way that respects the Node Module Resolution Algorithm. This might be interesting if, for example, you wanted to load TypeScript definition files to seed something like the monaco-editor.
  4. You want to build tooling that requires access to a module dependency graph. For example, you might want to show the set of files in a dependency graph and their inter-dependencies.

๐Ÿ—บ Velcro module resolution

Velcro starts from the principle that we cannot assume anything about the environment in which it runs (beyond that it has some baseline JavaScript primitives). Given this assumption, it follows that we cannot rely on having access to tools like npm or even a filesystem.

Without a file system, Velcro takes the stance that all source modules (files) should be addressable by a canonical url. In a world where modules are identified by urls, Velcro can allow situations where some files come from an in-memory memory:///index.js scheme, others from the filesystem at file:///index.js and yet others can come from a CDN like unpkg.com at https://unpkg.com/[email protected]/index.js.

Typically, the transition from one url scheme to another happens at the 'bare module' boundary. A bare module boundary is when one module expresses a dependency on something that is neither a relative nor absolute path. In Velcro, for example, a common pattern is to use the CompoundStrategy to join the CdnStrategy to something like the FsStrategy or MemoryStrategy strategies.

Example:

const cdnStrategy = CdnStrategy.forJsDelivr(readUrlFunction);
const memoryStrategy = new MemoryStrategy({
  '/index.js': 'module.exports = require("react");',
  '/package.json': JSON.stringify({
    name: '@@velcro/execute',
    version: '0.0.0',
    dependencies: {
      react: '^16.13.0',
    },
  }),
});
const compoundStrategy = new CompoundStrategy({ strategies: [cdnStrategy, memoryStrategy] });

As you can see, Velcro relies heavily on implementations of the ResolverStrategy interface to perform its functions. The design of the ResolverStrategy interface is such that it should be easy to compose.

You may, for example, write a caching strategy that sits behind a compound strategy but in front of a 'slow' strategy like a CDN. This caching strategy would be able to serve cache hits from cache and delegate misses to the child, CDN strategy.

Different resolver strategies can be composed together so that the final, top-level strategy that you pass to the Resolver has the exact behaviour you are looking for.

๐Ÿ•ธ Dependency graph

Since we have something that can resolve modules and read their code, from any source, and from any JavaScript runtime, we have all the tools we need to build build out a graph of modules.

Velcro's @velcro/bundler package does exactly that. It takes some configuration settings and a Resolver that has been instantiated with a ResolverStrategy and is able to efficiently build out the dependency graph between modules.

The bundler is unusual in that since there is no npm, no yarn, no pnpm or any such tool it cannot rely on something else composing npm modules into a node_modules tree. Instead, it contains logic to parse each file to identify that file's dependencies so that the graph building can continue.

What is really interesting, is that since Velcro is a tightly-integrated system, it was build so that we can obtain a record of every file and directory that was consulted to resolve file B from file A. Each edge in the graph therefore contains a record of all logical files or directories that, if changed, would invalidate that edge. This allows Velcro's bundler to be designed to react efficiently, accurately -- and more importantly -- minimally to changes.

Similarly, if a resolver strategy was designed to transpile files on the fly, that strategy could indicate to the graph builder which files were consulted to generate the transpiled output. Changes to these files would then invalidate the file's node (not the edge).

๐Ÿ“ฆ Bundling

With a module dependency graph in hand, it is not a huge leap to be able to serialize that graph into executable form. Why not?

Hey! Let's build a browser-native JavaScript bundler!

The @velcro/bundler module can take a graph build using the buildGraph() function and split it into different logical chunks. You can provide your own heuristic for allocating files to chunks or you can let Velcro happily dump everything into a single Chunk.

A Chunk is a subset of the overall dependency graph. To serialize it, different methods are available to produce a Build from the chunk. A Build has methods to output the combined code according to the format chosen when building the chunk.

Oh, and did I forget to mention that source-maps are tracked the whole way through? In the browser? OF COURSE!

The source map for a build can be produced in one of several formats via getters on the Build instance.

๐Ÿ”Œ Plugins

Velcro bundling can be customized by providing a list of plugins to the Bundler. A plugin is an object with a name: string property and that implements any of the following hooks:

  • resolveEntrypoint(ctx, uri) => { uri, rootUri } | undefined: Resolve a file reference passed as one of the entrypoints to buildGraph. Note: This API may change to accept a string spec instead of a Uri. This hook will be run sequentially for each plugin until the first one resolves a value.
  • resolveDependency(ctx, dependency, fromModule): { uri, rootUri } | undefined: Resolve a dependency from an already-resolved source module. This may be a relative or absolute (?) Uri and may also be a bare module specifier. This allows a plugin to, for example, override the default resolution to inject (or mock) any dependency. This hook will be run sequentially for each plugin until the first one resolves a value.
  • load(ctx, uri): { code } | undefined: Given a resolved entrypoint or dependency, this hook allows a plugin to override how the code is loaded for a given Uri. This hook will be run sequentially for each plugin until the first one resolves a value.
  • transform(ctx, uri, code): { code, sourceMap } | undefined: Having loaded the code at a given Uri, this hook allows a plugin to provide custom logic to transform the loaded code into JavaScript. Note that the resulting JavaScript must be a CommonJS module. This hook will be run sequentially for all Plugins that provide it with the output of one feeding into the input of the next.

Any hook may optionally return a Promise for the specified result signature.

For details on the signature of each of these methods, please consult the API docs for plugins.

โœจ Magic

With this sort of pattern, different components can be composed to provide higher-level, opinionated tools like the @velcro/runner.

The runner is barely distinguishable from magic.

Given some code you want to run and the npm dependencies it might have, the runner will:

  1. Create a Resolver with a combination of the MemoryStrategy, the CdnStrategy and the CompoundStrategy.
  2. Load the graph of modules implied by your code and its dependencies by using the Resolver.
  3. Serialize the graph into an executable bundle an inject a Runtime.
  4. Call the require method of the returned Runtime instance and return the exports of your code.

Let's review: the runner allows any code to be run anywhere with no pre-existing conditions except a 112 kB (minified) UMD bundle.

Resolver Strategy

The resolver strategy interface represents the minimal set of operations that allow Velcro to operate efficiently across a wide variety of conceptual backends. Implementing this interface is what allows modules to be resolved across different media.

getUrlForBareModule

Note: Not all strategies need to implement this. In practice, at least one does if you want to be able to resolve bare module specifiers like "react".

interface ResolverStrategy {
  /**
   * Produce a url given the components of a bare module specifier.
   *
   * @param ctx A `ResolverContext` that should be used for making calls to other strategy methods
   * @param name The name of a bare module
   * @param spec The optional `@version` of a bare module specifier
   * @param path The optional path at the end of the bare module specifier
   */
  getUrlForBareModule?(
    ctx: ResolverContext,
    name: string,
    spec: string,
    path: string
  ): MaybeThenable<ResolverStrategy.BareModuleResult>;
}

getCanonicalUrl

interface ResolverStrategy {
  /**
   * Determine the canonical uri for a given uri.
   *
   * For example, you might consider symlink targets their canonicalized path or you might
   * consider the canonicalized path of https://unpkg.com/react to be
   * https://unpkg.com/[email protected]/index.js.
   *
   * Dealing only in canonical uris means that anything produced from those can be cached.
   *
   * @param ctx A `ResolverContext` that should be used for making calls to other strategy methods
   * @param uri The uri to canonicalize
   */
  getCanonicalUrl(
    ctx: ResolverContext,
    uri: Uri
  ): MaybeThenable<ResolverStrategy.CanonicalizeResult>;
}

getResolveRoot

interface ResolverStrategy {
  /**
   * Get the logical resolve root for a given uri.
   *
   * For example, a filesystem-based strategy might consider the root to be `file:///`. Or,
   * if it was scoped to /home/filearts, the root might be `file:///home/filearts/`.
   *
   * Any uri that is not a 'child' of the resolve root should be considered out of scope for a given
   * strategy.
   *
   * @param ctx A `ResolverContext` that should be used for making calls to other strategy methods
   * @param uri The uri for which the logical resolve root uri should be found
   */
  getResolveRoot(ctx: ResolverContext, uri: Uri): MaybeThenable<ResolverStrategy.ResolveRootResult>;
}

getSettings

Note: Any strategy extending the AbstractResolverStrategy does not need to implement this method as default behaviour is provided.

interface ResolverStrategy {
  /**
   * Get the settings for a given uri
   *
   * This indirection allows resolver strategies to have per-strategy or even per-uri settings.
   *
   * @param ctx A `ResolverContext` that should be used for making calls to other strategy methods
   * @param uri The uri for which to load settings
   */
  getSettings(ctx: ResolverContext, uri: Uri): MaybeThenable<ResolverStrategy.SettingsResult>;
}

listEntries

interface ResolverStrategy {
  /**
   * Produce a list of resolved entries that are direct children of the given uri.
   *
   * This is the moral equivalent to something like non-recursive `fs.readdir()`. It is only
   * designed to show files and folders (for now).
   *
   * @param ctx A `ResolverContext` that should be used for making calls to other strategy methods
   * @param uri The uri at which to list entries
   */
  listEntries(ctx: ResolverContext, uri: Uri): MaybeThenable<ResolverStrategy.ListEntriesResult>;
}

readFileContent

interface ResolverStrategy {
  /**
   * Read the content at the uri as an `ArrayBuffer`
   *
   * ArrayBuffers are the lowest-common-denominator across the web and node and can easily be
   * decoded with standard web apis like `StringDecoder`. In Node.js, `Buffer` objects are also
   * `ArrayBuffer`s, allowing the tooling to be built on that primitive.
   *
   * This is helpful for the understanding that not all uris are expected to produce meaningful
   * text representations.
   *
   * @param ctx A `ResolverContext` that should be used for making calls to other strategy methods
   * @param uri The uri at which to read the content
   */
  readFileContent(
    ctx: ResolverContext,
    uri: Uri
  ): MaybeThenable<ResolverStrategy.ReadFileContentResult>;
}

Contributing

Velcro is organized as a monorepo with inter-module dependencies managed by lerna.

Initial setup:

# Install top-level developement dependencies
npm install

# Bootstrap package-level dependencies and set up symlinks between packages
npx lerna bootstrap

Running tests:

Running tests currently does not rely on having built packages. Jest is used with ts-jest to run unit and integration tests. Jest is set up such that each package is its own logical project and a further project is configured for top-level integration tests.

Jest is configured with moduleNameMapper settings that are designed to match the paths mappings in the tsconfig.json file.

Tests can be run via the test package script:

npm run test

Building:

Building velcro is also orchestrated by lerna and the actual building is done by Rollup.

npm run build

velcro's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

velcro's Issues

Support for non-js and non-json files

@alexswan10k you were asking about the possibility of including files other than .js and .json in Velcro.

This is something I do want to support but having tried the webpack loader approach in the past, I'm not sure that I want to go that route again. Webpack's loader ecosystem is heavily invested in a world where access to the file-system is taken for granted, which conflicts with Velcro's philosophical direction.

I think an approach we can consider right now is providing a mechanism to inject different _Parser_s.

There is already a layer of indirection where we could support pluggable parser plugins:

private getParserForUri(uri: Uri): ParserFunction {
const path = uri.path;
if (path.endsWith('.json')) {
return parseJson;
}
if (path.endsWith('.js')) {
return parseJavaScript;
}
throw new ParseError(uri, 'No suitable parser was found');
}
}

Right now, the contract is that any transformations of the underlying files need to be passed back to Velcro as arrays of CodeChanges:

export type CodeChange =
| {
type: 'appendRight';
start: number;
value: string;
}
| {
type: 'remove';
start: number;
end: number;
}
| {
type: 'overwrite';
start: number;
end: number;
value: string;
};

I'm not convinced that this is an approach that will continue to meet the different objectives of Velcro. I had decided to make the code changes raw json so that, for example, parsing could be delegated to a Worker. I think that's probably a useful constraint to keep in mind: the contract between a parser plugin and Velcro's bundler should be encodable in JSON.

My current thinking is along the lines below. There should be a conceptual difference between parsing and generating.

  • parsers: something that takes raw content and produces dependencies and basic transforms to produce an intermediate format.
  • generators (straw-man name): something that understands the intermediate format and is able to produce code in the desired target format (such as CommonJS, like we do now).

I don't know what the intermediate format should be. For example, Rollup uses ESM but that means introducing a CJS -> ESM transform which is quite a project in itself.

Velcro.Runtime package out of date?

Hi,

Just trying to assemble the bundler for a project alongside the runtime (using the published npm packages), but I am hitting a few bumps with respect to their being inconsistencies between the expected resolverHost for @velcro/runtime and the actual resolverHost in @velcro/resolver-host-compound. (isCacheable seems to be missing in newer).

Looking at the versions, it does appear that @velcro/runtime is the only package that hasn't been updated, so perhaps the types are just out of sync and the core runtime needs releasing? If not perhaps I am assembling these packages incorrectly, in which case perhaps you could point me in the right direction?

While I am at it, I am also not 100% sure what the best approach is for substituting real packages with the runtime.

Currently I am doing something along the lines of:

`
const memoryHost = new Velcro.ResolverHostMemory(
initialFiles,
"compound_host_with_cache"
);
const resolverHostUnpkg = new VelcroResolverHostUnpkg.ResolverHostUnpkg();
const resolverHost = new VelcroResolverHostCompound.ResolverHostCompound({
"https://unpkg.com/": resolverHostUnpkg as any,
[memoryHost.urlFromPath("/").href]: memoryHost
});

const runtime = Velcro.createRuntime({
cache: _cache,
injectGlobal: Velcro.injectGlobalFromUnpkg,
resolveBareModule: async (a, b, name, d) => {
if (name === "react") return memoryHost.urlFromPath("/react").href;
if (name === "react-dom")
return memoryHost.urlFromPath("/react-dom").href;
if (name === "prop-types")
return memoryHost.urlFromPath("/prop-types").href;

  const res = await Velcro.resolveBareModuleToUnpkg(a, b, name, d);
  return res;
},
resolverHost, 
rules: [
   ...
    ]
  }

]

});
`

And then after assembling, I can do the following:

runtime.set(memoryHost.urlFromPath("/react").href, React); runtime.set(memoryHost.urlFromPath("/react-dom").href, ReactDom);

Although this might work, it doesn't seem to be consistent with the bundler api, and also it doesn't work for underlying webpack plugins like css-loader or style-loader. It would be nice to do this because stuff like sass-loader has tons of dependencies and is slow on first pass.

Any thoughts?

Setting up the environment

Thought I would just post a new issue for this to iron everything out, as I think there are still some steps missing.

Steps so far:
npm install lerna -g
(I tried npx but it just kept blowing up on "could not find lerna@latest)

goto root
git clean -fdx
npm install
lerna bootstrap
npm run test

Tests run, but all fail, following error:
ReferenceError: describe is not defined.

Have tried:
https://stackoverflow.com/questions/55807824/describe-is-not-defined-when-installing-jest
and installing:
https://www.npmjs.com/package/eslint-plugin-jest
but no avail.

Any thoughts?

(query) - ESModules

What are thoughts on ESModules and how they might fit into Velcro?

I appreciate that this has been the future for 10 years now, and because of the lack of bare import specifiers, they have been mostly useless.

I was chatting to one of the guys over at bit.dev, and he mentioned that their package CDN will be based around esmodules as they have somewhat more control over the output process. He also mentioned the notion of import maps which may well be the missing link required.

Graph builder error "no such directory memory:/"

Error:

react-dom.development.js:11865 Uncaught GraphBuildError: Graph building failed with errors:
No such directory memory:/ at
 GraphBuilder.doAddUnresolvedUri:memory:/index.js
    at GraphBuilder.buildGraph (http://localhost:8080/public/148.js:1884:19)

Code sample as follows:

import * as Velcro from "@velcro/runner";
...
    const code = `
      console.log("I am a trivial sample");
    `;
    Velcro.execute(code, {
      readUrl: u => {
        console.log("fetching", u)
        return fetch(u).then(u => u.arrayBuffer())
      },
      external: (x) => {
        return false;
      },
      injectModules: {
      },
      dependencies: {},
      nodeEnv: "production"
    })

As discussed this also fails upon the (sandbox demo)[https://plnkr.co/edit/sDnzv3QEv6wudM9P].

Original findings:

I have had a go at trying to hook all this up, with little avail unfortunately. I keep hitting the same error as seen on the sandbox, after having tried various combinations of the new runner or composing these constructs myself.

I am pretty sure I have narrowed it down to the listEntries first line in memoryStrategy.ts

The offending line:

Uri.ensureTrailingSlash(uri).fsPath
where fsPath seems to be returning a backslash rather than a forward slash, which is then fed into getEntryAtPath, which then resolves a parent as undefined, as no segment/child can be found matching "" obviously!

I cant seem to locate fsPath on Uri or the builtin URL prototype, so perhaps you can help me work out where this is coming from?

I also noticed an inconsistency between Uri.parse('memory:///') and the generated Uri("memory:/directory/path"). Not sure if this is a problem though, i just cant rule it out yet either.

StackOverflow when running bundles with require circular references

Me again, sorry!

So I have been experimenting with some non-trivial packages and am hitting a couple of issues:
When using "react-alice-carousel" for example, I get a stack overflow when running the bundle. I am pretty sure it is due to two files pointing to eachother, and the way the runner does not cache the runtime result of the reference.

https://cdn.jsdelivr.net/npm/[email protected]/lib/utils/animation.js
https://cdn.jsdelivr.net/npm/[email protected]/lib/utils/index.js

Unfortunately I still cannot get the tests running locally or i would be more than happy to dig right in and have a look myself. Is it perhaps worth noting on the readme.md the exact build steps required and any external dependencies required from a git clean -fdx?

I have had a go at putting together a test, but I am massively guessing without being able to run:

  it('will run more complex package using react-dom/server', async () => {
    const code = `
      const React = require('react');
      const ReactDOMServer = require('react-dom/server');
      const Carousel = require("react-alice-carousel").default;

      const comp = React.createElement(Carousel, null, 'hello world');

      module.exports = ReactDOMServer.renderToString(comp);
    `;
    const result = await execute(code, {
      dependencies: {
        react: '^16.13.1',
        'react-dom': '^16.13.1',
        'react-alice-carousel': '1.18.0'
      },
      readUrl,
      nodeEnv: 'production',
    });

    expect(result).toEqual('<h1 data-reactroot=""> something </h1>');
  });

There is also another problem here that probably warrants its own issue, but I wanted to discuss before making waves: non js or json files. Many react component packages import css files. I know previously it was mentioned you didn't want to commit too much to the webpack ecosystem, only loaders do solve the specific problem of integrating custom content files into a bundle. I understand if this interface is hard to make fit, but perhaps a plugin system to do this sort of thing might be useful? I actually need more control than a basic css loader anyway so would be happy to handroll something here myself if need be. At the moment these parsers are hard coded in graphBuilder:getParserForUri.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.