Giter Club home page Giter Club logo

remix-params-helper's Introduction

Remix Params Helper

All Contributors

This package makes it simple to use Zod with standard URLSearchParams and FormData which are typically used in Remix apps.

🚨 Breaking Change v0.3.0

Helper no longer requires explicit types on helper. Thanks @zolrath. This will definitely cut down on the boilerplate.

🚧 Work in progress

This package is still work in progress. I'll be refining the API and fixing the TypeScript types.

πŸ›  Installation

npm install remix-params-helper zod

Zod is a peer dependency

πŸ“¦ Zod

Zod is used to validate untyped data and either return a valid object or a list of errors encounted.

To use the helper, first define your Zod schema. It also supports nested objects and arrays.

const ParamsSchema = z.object({
  a: z.number(),
  b: z.string(),
  c: z.boolean(),
  d: z.string().optional(),
  e: z.array(z.number()),
})

πŸ“ API Reference

getParams(params, schema)

This function is used to parse and validate data from URLSearchParams, FormData, or Remix params object.

It returns an object that has success property. If result.success is true then result.data will be a valid object of type T, inferred from your Zod schema.

Otherwise, result.errors will be an object with keys for each property that failed validation. The key value will be the validation error message.

NOTE: Error messages will now return the message from directly Zod. You can customize the error message in your Zod schema Zod Custom Error Messages

If the validation returns multiple errors for the same key, it will return an array, otherwise it will be a string.

errors[key] = 'message'
errors[key] = ['message 1', 'message 2']

Unlike Object.fromEntries(), this function also supports multi-value keys and will convert them to an array. So e=1&e=2&e=3 will convert it to e: [1,2,3]

const url = new URL(request.url)
const result = getParams(url.searchParams, ParamsSchema)
if (!result.success) {
  throw new Response(result.errors, { status: 400 })
}
// these variables will be typed and valid
const { a, b, c, d, e } = result.data

getSearchParams(request, schema)

This helper function is used to parse and validate URLSearchParams data from the Request found in the Remix action/loader, it returns the same result values as getParams.

const result = getSearchParams(request, ParamsSchema)
if (!result.success) {
  return json(result.errors, { status: 400 })
}
// these variable will be typed and valid
const { a, b, c, d, e } = result.data

getFormData(request, schema)

This helper function is used to parse and validate FormData data from the Request found in the Remix action/loader, it returns the same result values as getParams.

const result = await getFormData(request, ParamsSchema)
if (!result.success) {
  return json(result.errors, { status: 400 })
}
// these variables will be typed and valid
const { a, b, c, d, e } = result.data

✨ New in v0.4.2 Added *OrFail() versions of the helpers

The functions getParamsOrFail(), getFormDataOrFail(), getSearchParamsOrFail() will throw an Error when parsing fails. Since the helper can only return a valid result, the return value is always the data.

// returns valid data that can be destructured or Error is thrown
const { a, b, c, d, e } = await getFormDataOrFail(request, ParamsSchema)

NOTE: Although we provide these helpers, it is recommended that you return errors instead of throwing. Form validation is typically an expected error. Throwing Error should be reserved for unexpected errors.

✨ New in v0.4.0 Support for nested objects and arrays

Input names should be dot-separated (e.g, address.street). Array names can include the square brackets (e.g., favoriteFoods[]). These are optional. The helper will correctly determine if the value is an array.

describe('test nested objects and arrays', () => {
  it('should validate nested object', () => {
    const mySchema = z.object({
      name: z.string(),
      address: z.object({
        street: z.string(),
        city: z.string(),
        state: z.string(),
        zip: z.string(),
      }),
    })
    const formData = new FormData()
    formData.set('name', 'abcdef')
    formData.set('address.street', '123 Main St')
    formData.set('address.city', 'Anytown')
    formData.set('address.state', 'US')
    formData.set('address.zip', '12345')
    const result = getParams(formData, mySchema)
    expect(result.success).toBe(true)
    expect(result.data.address.street).toBe('123 Main St')
  })
  it('should validate arrays with [] syntax', () => {
    const mySchema = z.object({
      name: z.string(),
      favoriteFoods: z.array(z.string()),
    })
    const formData = new FormData()
    formData.set('name', 'abcdef')
    formData.append('favoriteFoods[]', 'Pizza')
    formData.append('favoriteFoods[]', 'Tacos')
    formData.append('favoriteFoods[]', 'Hamburgers')
    formData.append('favoriteFoods[]', 'Sushi')
    const result = getParams(formData, mySchema)
    expect(result.success).toBe(true)
    expect(result.data.favoriteFoods?.length).toBe(4)
  })
})

useFormInputProps(schema)

This helper allows you to set the props on your form <input/> based on your Zod schema.

The function returns another function that you use to spread the properties on your input. It currently sets the following props based on the key value you specify. If you need to override any of the props, just add it after you spread.

  • name
  • type: text, number, checkbox, date, email, url
  • required: not .optional()
  • min/max: number
  • minlength/maxlength: text
  • pattern: regex

If the key doesn't exist in the schema, it will throw an error. This way if you rename any properties, it will force you to use the correct key.

This currently uses the native browser validation like required. I plan on adding enhanced client-side validation that will utilize the same Zod schema.

function Component() {
  const inputProps = useFormInputProps(schema)

  return (
    <Form>
      <input ...{inputProps('a')} />
      <input ...{inputProps('b')} />
      <input ...{inputProps('c')} />
      {/* This will throw an error since 'x' is not in schema*/}
      <input ...{inputProps('x')} />
    </Form>
  )
}

🌎 Example App

There is an example app at https://remix-params-helper.herokuapp.com/

Click on the Actions demo and the URL Params demo to see the helper in action.

😍 Contributors

Thanks goes to these wonderful people (emoji key):


Kiliman

πŸ’» πŸ“–

Antti

πŸ’»

Matt Furden

πŸ’»

RaΓΊl R Pearson

πŸ’»

Clifford Fajardo

πŸ“–

Benjamin

⚠️ πŸ›

Dusty Doris

πŸ’» πŸ›

This project follows the all-contributors specification. Contributions of any kind welcome!

remix-params-helper's People

Contributors

cliffordfajardo avatar dusty avatar kettui avatar kiliman avatar raulrpearson avatar zolrath avatar

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

remix-params-helper's Issues

Incorrect parsing from string to number with .refine()

As discussed on Discord ;)

When using inputs with type number, the conversion work with the following schema:

const schema = z
  .object({
    min: z.number(),
    max: z.number(),
  })

But not with this one (with .refine()):

const schema = z
  .object({
    min: z.number(),
    max: z.number(),
  })
  .refine((data) => data.min < data.max, {
    message: "Min < Max",
    path: ["min"],
  })

I receive the error:

Expected number, received string

Unused variables in error construction logic

Unless I'm missing something, the value and prop variables here are unused:

for (let issue of result.error.issues) {
const { message, path, code, expected, received } = issue
const [key, index] = path
let value = o[key]
let prop = key
if (index !== undefined) {
value = value[index]
prop = `${key}[${index}]`
}
addError(key, message)
}
return { success: false, data: undefined, errors }

Also more generally, I'm not really clear on why the errors are being manipulated.

Would it not make more sense to just return the raw output of safeParse and allow the user to use something like result.error.flatten()?

Support Union?

I have a base set of search params and I want to union it with another set of search params, right now that's not working for me. Here's an example:

export const BaseSearchParamsSchema = z.object({
	query: z.string().default(''),
	exclude: z.array(z.string()).default([]),
})

const NullableNumber = z
	.string()
	.nullable()
	.optional()
	.transform(s => {
		const number = s ? Number(s) : null
		return number === null || Number.isNaN(number) ? null : number
	})

export const SearchParamsSchema = z.union([
	BaseSearchParamsSchema,
	z.object({
		lat: NullableNumber,
		long: NullableNumber,
	}),
])

export async function loader({ request }: LoaderArgs) {
	// these all show an error
	const { query, lat, long, exclude } = getSearchParamsOrFail(
		request,
		SearchParamsSchema,
	)
	// for example:
	// Property 'exclude' does not exist on type '{ exclude: string[]; query: string; } | { lat: number | null; long: number | null; }'.ts(2339)
}

Tabular input

Describe the bug

Hello,
I'm trying to use remix-params-helper to validate input via zod. Unfortunately, I don't know how to handle tabular input.

Let's say my schema contains the following (some fields are omitted):

omittedField: z.string(),
inventory: z.array(
    z.object({
        location: z.string(),
        quantity: z.number()
    })
)

How can I specify my <input>s to match the array schema? I tried:

<input type="hidden"
       name="inventory[].location"
       value="blah"
/>

<input name="inventory[].quantity" />

I also tried inventory[insert-index-here].location, to no avail. The library just throws an error: Cannot read properties of undefined (reading 'location'). Am I missing something? If not, how can I work around this issue?

Your Example Website or App

https://stackblitz.com/edit/typescript-gtvsz9?file=index.ts

Steps to Reproduce the Bug or Issue

  1. Use arrays with objects.
  2. Watch the code crash and burn.

Expected behavior

I expected tabular input to work with objects.

Screenshots or Videos

No response

Platform

  • OS: macOS
  • Browser: Safari
  • Version: 15.4

Additional context

No response

Unexpected type ZodAny using instanceof

I have a schema for uploading files using the remix fileuploader api.

const schema = z.object({
  files: z.array(z.instanceof(NodeOnDiskFile),
});

Trying to parse the form data with this schema throws Unexpected type ZodAny for key files

Unexpected type ZodEffects

I have this schema to test the content of a JSON value.

import { z } from "zod";

const schema = z.object({
  name: z.string().refine((val) => {
    const values = Object.values(JSON.parse(val));
    return (
      values.length > 0 &&
      values.some((v) => typeof v === "string" && v.length > 0)
    );
  }, "You must fill in at least one language")
});

// error
schema.parse({ name: '{""}' });
schema.parse({ name: '{"en":""}' });
schema.parse({ name: '{"en":"","fr":""}' });

// success
schema.parse({ name: '{"en":"Game"}' });
schema.parse({ name: '{"en":"Game","fr":""}' });
schema.parse({ name: '{"en":"Game","fr":"Jeu"}' });

The schema works, but remix-params-helper does not seem to like it..

Error: Unexpected type ZodEffects for key name
    at processDef (/Users/benjamin/Projects/horeka/node_modules/remix-params-helper/dist/cjs/helper.js:149:15)
    at parseParams (/Users/benjamin/Projects/horeka/node_modules/remix-params-helper/dist/cjs/helper.js:19:9)
    at getParamsInternal (/Users/benjamin/Projects/horeka/node_modules/remix-params-helper/dist/cjs/helper.js:50:9)
    at getParams (/Users/benjamin/Projects/horeka/node_modules/remix-params-helper/dist/cjs/helper.js:84:12)
    at action (/Users/benjamin/Projects/horeka/build/index.js:993:69)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.callRouteAction (/Users/benjamin/Projects/horeka/node_modules/@remix-run/server-runtime/data.js:36:14)
    at async handleDataRequest (/Users/benjamin/Projects/horeka/node_modules/@remix-run/server-runtime/server.js:110:18)
    at async requestHandler (/Users/benjamin/Projects/horeka/node_modules/@remix-run/server-runtime/server.js:45:20)
    at async /Users/benjamin/Projects/horeka/node_modules/@remix-run/express/server.js:45:22

Support refine/ZodEffects in useFormInputProps

Passing a ZodEffects (output of using refine) into useFormInputProps will result in inputProps function that cannot find any of your keys.

I believe the problem is here where it is assuming there is a schema.shape when there may actually be a schema._def.schema.shape:

const shape = schema.shape

Current work around:

  const realSchema = schema instanceof ZodEffects ? schema._def.schema : schema;
  const inputProps = useFormInputProps(realSchema);

Can't find declaration file in 0.5.0

Receiving the following TypeScript error after upgrading to 0.5.0

Could not find a declaration file for module 'remix-params-helper'. 

I am using Remix 2.0 with Vite and ESM modules.
If I go back to 0.4.10, the problem goes away.

Custom validation messages?

Thanks for writing this--this makes working with params in Remix much easier!

Have you considered adding customization to the parameter validation messages? The current messages are pretty inflexible. In particular, https://github.com/kiliman/remix-params-helper/blob/main/src/helper.ts#L67 leads to messages like "must be at least 8 characters for password" that sound pretty awkward. I could rewrite the message to improve this, like "8 is the required length for password", but that's not much better. Something like "${prop}${message}" would work better for my use case here, but I suspect that's not universally true (e.g., in some cases, if you're placing the message right next to the field that failed validation, you may want to not repeat the field name and have the error just pass through the zod validation error).

Maybe a callback that formats the Zod errors would work? Thoughts?

Always create array if key contains `[]`

Currently when passing foo[]=abc, the helper strips the brackets and treats it as a string. It only generates an array if there are multiple values with same key.

If the key contains [] always treat it as an array even if there is just one value.

support throwing an error on an unsuccessful getParams, getFormData instead of allowing `.data` being undefined

Describe the bug

Often I use getParams like so. I wish getParams just throw an exception on unsuccessful parse instead of me having to check for it everytime

const parsedFormData = await getParams(formData, someSchema)

// I wish I didn't have to do this every time
if (!parsedFormData.data) throw new Error("no data")

// do something with
parsedFormData.data.someField

Your Example Website or App

every app

Steps to Reproduce the Bug or Issue

as listed

Expected behavior

throwing an exception

Screenshots or Videos

No response

Platform

server

Additional context

No response

Support for parseAsync

Hello! First, thanks for this package, it is very useful to hit the ground running with Remix.

I'm wondering if you have any plans to support zod's async parsing. I just tried to use an asynchronous refinement and got a

Error: Async refinement encountered during synchronous parse operation. Use .parseAsync instead.

I'd love to be the guy that raises a PR for it, but tbh I'm a cloud engineer far from land here 😁

Support for parsing `type=date` from `z.string().pipe(z.coerce.date())`

In my schema I use z.string().pipe(z.coerce.date()).transform((date) => toServerDate(date)) for a date field. I want to validate that it's a string containing a valid date (and then output it in a particular format).

This isn't picked up as type=date because it isn't an instanceof ZodDate, instead it involves some nesting of ZodEffects, ZodPipeline and potentially ZodOptional (handled).

It looks like the handling in processDef covers more cases than getInputProps but isn't used in the useFormInputProps(..) workflow.

I will put up a PR shortly that suggests a possible change.

Rename?

Just a thought. There's nothing remix-specific in this package. This should probably be "zod-params-helper".

getFormData throw an error when ZodLiteral is used in schema

Describe the bug

When a zod schema contains a field of type Literal, the method getFormData throw the following error:

Error: Unexpected type ZodLiteral for key **key_name_here** at processDef (/home/florent/dev/pro/scrumfit-v2/node_modules/remix-params-helper/dist/cjs/helper.js:187:15) at parseParams (/home/florent/dev/pro/scrumfit-v2/node_modules/remix-params-helper/dist/cjs/helper.js:31:9) at getParamsInternal (/home/florent/dev/pro/scrumfit-v2/node_modules/remix-params-helper/dist/cjs/helper.js:62:9) at getFormData (/home/florent/dev/pro/scrumfit-v2/node_modules/remix-params-helper/dist/cjs/helper.js:106:12)

Your Example Website or App

https://github.com/webflo-dev/issue-zod-literal

Steps to Reproduce the Bug or Issue

const schema = z.object({
  someKey: z.literal('specific_value_here'),
});

try {
   const formData = await getFormData(request.clone(), schema);
   console.log(formData.success ? 'validation successful' : 'validation failed');
} catch {
   console.log('should not go here');
}

<Form method="post">
   <input type="hidden" name="someKey" value="deleteeeeee" />
   <button type="submit" name="id" value="1">delete</button>
 </Form>

Expected behavior

Based on the below code, we should have log "validation failed" but we have "should not go here".

However, it works if the key is missing. But if the key is present with an unexpected value, it throw an error.

After some quick investigation, I seems that "ZodLiteral" type is not taken in account properly and the error is thrown from here: https://github.com/kiliman/remix-params-helper/blob/main/src/helper.ts#L234

Screenshots or Videos

No response

Platform

  • OS: Linux
  • nodeJS version 16.14.0

Additional context

No response

inputProps() returning `required` even for `optional()` schema

Describe the bug

inputProps() returning required even for optional() schema

Your Example Website or App

n/a

Steps to Reproduce the Bug or Issue

n/a

Expected behavior

n/a

Screenshots or Videos

No response

Platform

  • OS: [e.g. macOS, Windows, Linux]
  • Browser: [e.g. Chrome, Safari, Firefox]
  • Version: [e.g. 91.1]

Additional context

No response

z.NativeEnum support

Hello!
Are there any plans to support zod's native enums?
I had hoped to restrict a param to a list of options from my Prisma client but remix-params-helper does not seem to like it.

Using this as a simplified example:

enum TestEnum {
	A = "A",
	B = "B",
}

const ParamsSchema = z.object({
	test: z.nativeEnum(TestEnum).optional(),
	q: z.string().optional(),
});
type ParamsType = z.infer<typeof ParamsSchema>;

export const loader: LoaderFunction = async ({ request }) => {
	let url = new URL(request.url);
	const result = getParams<ParamsType>(url.searchParams, ParamsSchema);
	console.log(result);
        return null;
}

Using the q param is fine, but as soon as I attempt to use the test param we blow up:

[1] GET /posts?test=A 500 - - 62.162 ms
[1] There was an error running the data loader for route routes/posts
[1] Error: Unexpected type ZodNativeEnum for key test
[1]     at processDef (.../node_modules/remix-params-helper/dist/cjs/helper.js:115:15)
[1]     at processDef (.../node_modules/remix-params-helper/dist/cjs/helper.js:102:9)
[1]     at getParams (.../node_modules/remix-params-helper/dist/cjs/helper.js:17:13)

Thanks for making this library, so far its otherwise been a delight!

Throw a json response?

This is not an issue, rather a discussion.

Is there a way to reduce this part (<--) of the code?

if (action === "save") {
  const result = getParams<SaveActionType>(formData, saveSchema);

  if (!result.success) {                            // <--
    return json(result.errors, { status: 400 });    // <--
  }                                                 // <--

  const { id, ...values } = result.data;
  await updateExam(id, values);
}

In fact, my real question is: does a throw in getParams could be captured and transformed, in some way, in the returned json via the CatchBoundary?

If I have several actions, it seems repetitive. Perhaps can it be improved? – I'm fully aware that at some point, things need to be written, just asking ;D

Support for `yup`

yup is a very popular alternative to Zod. Is there any way we can use it as an alternative?

Unable to parse FormData params using prod build

I've bumped into a strange issue that seems to only occur while running a prod build of the app. When running the app in dev, the FormData is parsed as expected, but in prod an empty object is returned. From what I've seen, it appears to be related to checking shape. In dev, shape is updated as expected, but in prod {} is returned because shape instanceof ZodObject || shape instanceof ZodEffects is falsy so shape isn't set correctly.

The following repo contains an example of what I'm seeing. Running npm run dev, entering a term on the "FormData Test" tab, and hitting Save will show the posted data correctly, while running npm run build && npm run preview will show an empty object after entering a term and hitting Save.

https://github.com/corydeppen/params-helper-sveltekit

SyntaxError when using zod `.preprocess()`

Describe the bug

SyntaxError when using zod .preprocess().

Schema seems to work correctly when using zod .parse(). I noticed the same schema doesn't seem to be supported when used with getParamsOrFail().

Steps to Reproduce the Bug or Issue

const schema = z.object({
  id: z.string(),
  intervals: z.preprocess(
    (val) => (typeof val === "string" ? JSON.parse(val) : []),
    z.array(
      z.object({
        // id: z.number(),
        day: z.number().min(0).max(6),
        from: z.string().regex(/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/),
        to: z.string().regex(/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/),
      })
    )
  ),
  intervalIds: z.preprocess(
    (val) => (typeof val === "string" ? JSON.parse(val) : []),
    z.array(z.number())
  ),
});

const intervals = formData.get("intervals");
const intervalIds = formData.get("intervalIds");
console.log({ intervals, intervalIds });

// works βœ… 
const dataZod = schema.parse({ id: "test", intervals, intervalIds });
console.log({ dataZod });

// throws error πŸ”΄ 
const data = await getParamsOrFail(formData, schema);
console.log({ data });

Output:

{
  intervals: '[{"id":1,"day":1,"from":"07:00","to":"23:30"},{"id":2,"day":2,"from":"07:00","to":"23:30"},{"id":3,"day":3,"from":"07:00","to":"23:30"},{"id":4,"day":4,"from":"07:00","to":"23:30"},{"id":5,"day":5,"from":"07:00","to":"23:30"},{"id":6,"day":6,"from":"07:00","to":"23:30"}]',
  intervalIds: '[1,2,3,4,5,6]'
}
{
  dataZod: {
    id: 'test',
    intervals: [ [Object], [Object], [Object], [Object], [Object], [Object] ],
    intervalIds: [ 1, 2, 3, 4, 5, 6 ]
  }
}
SyntaxError: Unexpected token U in JSON at position 0
    at JSON.parse (<anonymous>)

Expected behavior

{
  data: {
    id: 'test',
    intervals: [ [Object], [Object], [Object], [Object], [Object], [Object] ],
    intervalIds: [ 1, 2, 3, 4, 5, 6 ]
  }
}

Support for ZodIntersection (and maybe ZodUnion?)

Similar to #25, if your schema is a z.intersection(schema1, schema2) or z.union([...schemas]), it won't work with the hook.

As you know, these schemas can get pretty complicated. In the case of intersection there are 2 branches of other zod types to collect fields from. Here's an initial stab that seems to work ok for my use case but could definitely be improved:

function getShapeFromZodType(schema: ZodType, shape: any = {}) {
  if (schema instanceof ZodObject) {
    for (const key of Object.keys(schema.shape)) {
      shape[key] = schema.shape[key]
    }

    return shape
  }

  if (schema instanceof ZodEffects) {
    return getShapeFromZodType(schema._def.schema, shape)
  }

  if (schema instanceof ZodIntersection) {
    return {
      ...getShapeFromZodType(schema._def.left, shape),
      ...getShapeFromZodType(schema._def.right, shape),
    }
  }
}

export function useFormInputProps(schema: ZodType) {
  const shape = getShapeFromZodType(schema)

  return function props(key: string) {
    const def = shape[key]
    if (!def) {
      throw new Error(`no such key: ${key}`)
    }
    return getInputProps(key, def)
  }
}

Support z.enum in useFormInputProps by returning options

It'd be helpful if the library could figure out the options (as it does with name and type) so I can pass it to my generic FormField component which knows how to display the correct form element based on type.

Not sure if the type returned should be enum or string. Either way I can figure it out via the presence of options.

export const schema = z.object({
  fish: z.enum(["Salmon", "Tuna", "Trout"]).optional(),
});

const inputProps = useFormInputProps(schema);

// Want
<FormField options={inputProps('fish').options}/>

Will probably also need to make sure this works in the case of using refine, something like:

export const schema = z.object({
  fish: z.enum(["Salmon", "Tuna", "Trout"]).optional(),
}).refine((data) => data.fish == "Tuna", {
  message: "Don't choose Tuna!",
});

Current work around is to grab the options myself (not sure if this supports the refine case yet):

  const options = schema.shape[field]?.unwrap?.().options;

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.