├── .all-contributorsrc ├── .babelrc ├── .changeset ├── README.md └── config.js ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .gitignore ├── .huskyrc.js ├── .prettierignore ├── .prettierrc.js ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUTURE.md ├── LICENSE ├── MINUTIAE.md ├── README.md ├── docs ├── react-codesandboxer-example.gif └── vs-codesandboxer-example.gif ├── fixtures ├── importResolution │ ├── css │ │ └── A.css │ ├── fromIndex │ │ ├── js │ │ │ └── index.js │ │ ├── json │ │ │ └── index.json │ │ └── jsx │ │ │ └── index.jsx │ ├── js │ │ └── A.js │ ├── json │ │ └── A.json │ ├── jsx │ │ ├── A.jsx │ │ └── B.jsx │ ├── sass │ │ ├── A.sass │ │ └── B.sass │ ├── scss │ │ ├── A.scss │ │ └── B.scss │ ├── ts │ │ ├── A.ts │ │ └── B.ts │ ├── tsx │ │ ├── A.tsx │ │ └── B.tsx │ └── vue │ │ ├── A.vue │ │ └── B.vue ├── look-what-we-can-do │ ├── hidden-bonus.js │ └── index.js ├── scoped │ ├── index.js │ └── package.json ├── simple.js ├── simpleVue.vue ├── testImage.png ├── withAbsoluteImport.js ├── withCssImport.js ├── withCssImportNoDeclaration.js ├── withJSONImport.js ├── withPNG.js ├── withRelativeImport.js ├── withSass.js └── withScss.js ├── flow-typed └── npm │ └── jest_v22.x.x.js ├── package.json ├── packages ├── bitbucket-codesandboxer │ ├── CHANGELOG.md │ ├── bookmarklet.js │ ├── package.json │ ├── src │ │ ├── atlassian-connect.json │ │ ├── components │ │ │ └── GitFileExplorer.js │ │ ├── pages │ │ │ ├── deploy-file │ │ │ │ ├── index.html │ │ │ │ ├── index.js │ │ │ │ └── loading.css │ │ │ ├── home │ │ │ │ ├── home.css │ │ │ │ ├── index.html │ │ │ │ └── index.js │ │ │ └── select-file │ │ │ │ ├── index.html │ │ │ │ └── index.js │ │ └── utils │ │ │ ├── .gitignore │ │ │ ├── bitbucket.js │ │ │ └── github.js │ └── webpack.config.js ├── codesandboxer-fs │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── index.js │ ├── package.json │ └── src │ │ ├── assembleFiles.js │ │ ├── assembleFilesAndPost.js │ │ ├── cli.js │ │ ├── constants.js │ │ ├── fs.test.js │ │ ├── loadFiles.js │ │ ├── loadRelativeFile.js │ │ ├── templates │ │ ├── index.js │ │ ├── react-typescript.js │ │ ├── react.js │ │ └── vue.js │ │ └── types.js ├── codesandboxer │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── constants.js │ │ ├── fetchFiles │ │ ├── __snapshots__ │ │ │ └── fetchFiles.test.js.snap │ │ ├── ensureExample.js │ │ ├── ensureExtension.js │ │ ├── ensureExtensionAndTemplate.js │ │ ├── ensurePkgJSON.js │ │ ├── fetchFiles.test.js │ │ ├── fetchInternalDependencies.js │ │ └── index.js │ │ ├── fetchRelativeFile │ │ ├── fetchRelativeFile.test.js │ │ ├── getUrl.js │ │ └── index.js │ │ ├── finaliseCSB │ │ ├── getParameters.js │ │ └── index.js │ │ ├── index.js │ │ ├── parseFile │ │ ├── __snapshots__ │ │ │ └── parseFile.test.js.snap │ │ ├── index.js │ │ ├── parseDeps.js │ │ ├── parseFile.js │ │ ├── parseFile.test.js │ │ ├── parseScssfile.js │ │ └── parseScssfile.test.js │ │ ├── replaceImports │ │ ├── index.js │ │ └── replaceImports.test.js │ │ ├── sendFilesToCSB │ │ └── index.js │ │ ├── templates │ │ ├── index.js │ │ ├── packagejson.js │ │ ├── react-typescript.js │ │ ├── react.js │ │ └── vue.js │ │ ├── types.js │ │ └── utils │ │ ├── absolutesToRelative.js │ │ ├── getAllImports.js │ │ ├── getRegexMatchStr.js │ │ ├── replaceImport.js │ │ ├── resolvePath.js │ │ └── utils.test.js ├── react-codesandboxer │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── CodeSandboxer.js │ │ └── index.js └── vs-codesandboxer │ ├── .eslintrc.json │ ├── .gitignore │ ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── settings.json │ ├── .vscodeignore │ ├── CHANGELOG.md │ ├── README.md │ ├── extension.js │ ├── logger.js │ ├── package.json │ └── vsc-extension-quickstart.md ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "codesandboxer", 3 | "projectOwner": "codesandbox", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "commitConvention": "none", 12 | "contributors": [ 13 | { 14 | "login": "Noviny", 15 | "name": "Ben Conolly", 16 | "avatar_url": "https://avatars1.githubusercontent.com/u/15622106?v=4", 17 | "profile": "https://github.com/Noviny", 18 | "contributions": [ 19 | "bug", 20 | "code", 21 | "doc", 22 | "ideas", 23 | "infra", 24 | "maintenance", 25 | "test", 26 | "tool" 27 | ] 28 | }, 29 | { 30 | "login": "jossmac", 31 | "name": "Joss Mackison", 32 | "avatar_url": "https://avatars3.githubusercontent.com/u/2730833?v=4", 33 | "profile": "https://twitter.com/JossMackison", 34 | "contributions": [ 35 | "code" 36 | ] 37 | }, 38 | { 39 | "login": "dominikwilkowski", 40 | "name": "Dominik Wilkowski", 41 | "avatar_url": "https://avatars3.githubusercontent.com/u/1266923?v=4", 42 | "profile": "https://dominik-wilkowski.com", 43 | "contributions": [ 44 | "content" 45 | ] 46 | }, 47 | { 48 | "login": "lukebatchelor", 49 | "name": "lukebatchelor", 50 | "avatar_url": "https://avatars2.githubusercontent.com/u/18694878?v=4", 51 | "profile": "https://github.com/lukebatchelor", 52 | "contributions": [ 53 | "code" 54 | ] 55 | }, 56 | { 57 | "login": "CompuIves", 58 | "name": "Ives van Hoorne", 59 | "avatar_url": "https://avatars3.githubusercontent.com/u/587016?v=4", 60 | "profile": "https://twitter.com/CompuIves", 61 | "contributions": [ 62 | "code", 63 | "test", 64 | "doc" 65 | ] 66 | }, 67 | { 68 | "login": "gillesdemey", 69 | "name": "Gilles De Mey", 70 | "avatar_url": "https://avatars1.githubusercontent.com/u/868844?v=4", 71 | "profile": "https://gilles.demey.io", 72 | "contributions": [ 73 | "code" 74 | ] 75 | }, 76 | { 77 | "login": "kangweichan", 78 | "name": "kangweichan", 79 | "avatar_url": "https://avatars1.githubusercontent.com/u/47547953?v=4", 80 | "profile": "https://github.com/kangweichan", 81 | "contributions": [ 82 | "code" 83 | ] 84 | }, 85 | { 86 | "login": "MichaelDeBoey", 87 | "name": "Michaël De Boey", 88 | "avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4", 89 | "profile": "https://michaeldeboey.be", 90 | "contributions": [ 91 | "maintenance", 92 | "tool" 93 | ] 94 | } 95 | ], 96 | "contributorsPerLine": 7 97 | } 98 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react", "flow"], 3 | "plugins": [ 4 | "transform-class-properties", 5 | "transform-object-rest-spread", 6 | "transform-runtime", 7 | "syntax-dynamic-import" 8 | ], 9 | "ignore": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by 4 | `@atlaskit/build-releases`, a build tool that works with `bolt` to help you 5 | release components from a mono-repository. You can find the full documentation 6 | for it [here](https://www.npmjs.com/package/@atlaskit/build-releases) 7 | 8 | To help you get started though, here are some things you should know about this 9 | folder: 10 | 11 | ## Changesets are automatically generated 12 | 13 | Changesets are generated by the `build-releases changeset` command, though it 14 | may have been given a new name within your repository. As long as you are 15 | following a changeset release flow, you shouldn't have any problems. 16 | 17 | ## Each changeset is its own folder 18 | 19 | We use hashes by default for these folder names to avoid collisions when 20 | generating them, but there's no harm that will come from renaming them. 21 | 22 | ## Changesets are automatically removed 23 | 24 | When `build-releases version` or equivalent command is run, all the changeset 25 | folders are removed. This is so we only ever use a changeset once. This makes 26 | this a very bad place to store any other information. 27 | 28 | ## Changesets come in two parts 29 | 30 | You should treat these parts quite differently: 31 | 32 | - `changes.md` is a file you should feel free to edit as much as you want. It 33 | will be prepended to your changelog when you next run your version command. 34 | - `changes.json` is a file that includes information about releases, what should 35 | be versioned by the version command. We strongly recommend against editing 36 | this directly, as you may make a new changeset that puts your bolt repository 37 | into an invalid state. 38 | 39 | ## I want to edit the information in a `changes.json` - how do I do it safely? 40 | 41 | The best option is to make a new changeset using the changeset command, copy 42 | over the `changes.md`, then delete the old changeset. 43 | 44 | ## Can I rename the folder for my changeset? 45 | 46 | Absolutely! We need unique hashes to make changesets play nicely with git, but 47 | changing your folder from our hash to your own name isn't going to cause any 48 | problems. 49 | 50 | ## Can I manually delete changesets? 51 | 52 | You can, but you should be aware this will remove the intent to release 53 | communicated by the changeset, and should be done with caution. 54 | -------------------------------------------------------------------------------- /.changeset/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Hey, welcome to the changeset config! This file has been generated 3 | for you with the default configs we use, and some comments around 4 | what options mean, so that it's easy to customise your workflow. 5 | 6 | You should update this as you need to craft your workflow. 7 | 8 | Config provided by a CI command takes precedence over the contents of this file. 9 | 10 | If a config option isn't present here, we will fall back to the defaults. 11 | */ 12 | 13 | const changesetOptions = { 14 | // If true, we will automatically commit the changeset when the command is run 15 | commit: false, 16 | }; 17 | 18 | // This function takes information about a changeset to generate an entry for it in your 19 | // changelog. We provide the full changeset object as well as the version. 20 | // It may be a good idea to replace the commit hash with a link to the commit. 21 | 22 | /* the default shape is: 23 | - [patch] ABCDEFG: 24 | 25 | A summary message you wrote, indented 26 | */ 27 | 28 | const getLink = commit => 29 | `https://github.com/codesandbox/codesandboxer/commit/${commit}`; 30 | 31 | const getReleaseLine = async (changeset, versionType) => { 32 | const indentedSummary = changeset.summary 33 | .split('\n') 34 | .map(l => ` ${l}`.trimRight()) 35 | .join('\n'); 36 | 37 | return `- [${versionType}] [${changeset.commit}](${getLink( 38 | changeset.commit 39 | )}):\n${indentedSummary}`; 40 | }; 41 | 42 | // This function takes information about what dependencies we are updating in the package. 43 | // It provides an array of related changesets, as well as the dependencies updated. 44 | 45 | /* 46 | - Updated dependencies: [ABCDEFG]: 47 | - Updated dependencies: [HIJKLMN]: 48 | - dependencyA@1.0.1 49 | - dependencyb@1.2.0 50 | */ 51 | const getDependencyReleaseLine = async (changesets, dependenciesUpdated) => { 52 | if (dependenciesUpdated.length === 0) return ''; 53 | 54 | const changesetLinks = changesets.map( 55 | changeset => 56 | `- Updated dependencies [${changeset.commit}](${getLink( 57 | changeset.commit 58 | )}):` 59 | ); 60 | 61 | const updatedDepenenciesList = dependenciesUpdated.map( 62 | dependency => ` - ${dependency.name}@${dependency.version}` 63 | ); 64 | 65 | return [...changesetLinks, ...updatedDepenenciesList].join('\n'); 66 | }; 67 | 68 | const versionOptions = { 69 | // If true, we will automatically commit the version updating when the command is run 70 | commit: false, 71 | // Adds a skipCI flag to the commit - only valid if `commit` is also true. 72 | skipCI: false, 73 | // Do not modify the `changelog.md` files for packages that are updated 74 | noChangelog: false, 75 | // A function that returns a string. It takes in options about a change. This allows you to customise your changelog entries 76 | getReleaseLine, 77 | // A function that returns a string. It takes in options about when a pacakge is updated because 78 | getDependencyReleaseLine, 79 | }; 80 | 81 | const publishOptions = { 82 | // This sets whether unpublished packages are public by default. We err on the side of caution here. 83 | public: false, 84 | }; 85 | 86 | module.exports = { 87 | versionOptions, 88 | changesetOptions, 89 | publishOptions, 90 | }; 91 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | flow-typed 4 | scratchings.js 5 | fixtures 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | env: { 4 | browser: true, 5 | es6: true, 6 | node: true, 7 | jest: true, 8 | }, 9 | plugins: ['react'], 10 | rules: { 11 | curly: ['error', 'multi-line'], 12 | 'jsx-quotes': 'error', 13 | 'no-shadow': 'warn', 14 | 'no-trailing-spaces': 'error', 15 | 'no-undef': 'error', 16 | 'no-underscore-dangle': 'error', 17 | 'no-unused-expressions': 'error', 18 | 'no-unused-vars': [ 19 | 'error', 20 | { 21 | args: 'after-used', 22 | ignoreRestSiblings: true, 23 | vars: 'all', 24 | }, 25 | ], 26 | 'object-curly-spacing': ['error', 'always'], 27 | 'react/jsx-boolean-value': 'warn', 28 | 'react/jsx-no-undef': 'error', 29 | 'react/jsx-uses-react': 'error', 30 | 'react/jsx-uses-vars': 'error', 31 | 'react/jsx-wrap-multilines': 'warn', 32 | 'react/no-did-mount-set-state': 'warn', 33 | 'react/no-did-update-set-state': 'warn', 34 | 'react/no-unknown-property': 'warn', 35 | 'react/react-in-jsx-scope': 'error', 36 | 'react/self-closing-comp': 'warn', 37 | 'react/sort-prop-types': 'warn', 38 | semi: 'error', 39 | strict: 'off', 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/dist/.* 3 | 4 | [include] 5 | 6 | [libs] 7 | 8 | [lints] 9 | 10 | [options] 11 | module.name_mapper='codesandboxer' -> '/packages/codesandboxer/src/index.js' 12 | 13 | [strict] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Dependency directory 6 | node_modules 7 | 8 | # dist 9 | dist 10 | 11 | # Other 12 | .DS_Store 13 | .env 14 | 15 | # My experimentation file 16 | scratchings.js 17 | 18 | # This file must never be checked in 19 | github-auth.js 20 | 21 | # Yarn files 22 | .yarn 23 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | 'pre-commit': 'pretty-quick --staged', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.json 2 | dist 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | endOfLine: 'lf', 3 | proseWrap: 'always', 4 | singleQuote: true, 5 | trailingComma: 'es5', 6 | }; 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | script: 3 | - touch packages/bitbucket-codesandboxer/src/utils/github-auth.js 4 | - bolt 5 | - pnpm run build 6 | - pnpm run test 7 | - pnpm run flow 8 | - pnpm run lint 9 | cache: pnpm 10 | node_js: 11 | - 'node' 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # CodeSandbox Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and 9 | expression, level of experience, education, socio-economic status, nationality, 10 | personal appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at code-of-conduct@codesandbox.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an 62 | incident. Further details of specific enforcement policies may be posted 63 | separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 72 | version 1.4, available at 73 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 74 | 75 | [homepage]: https://www.contributor-covenant.org 76 | 77 | For answers to common questions about this code of conduct, see 78 | https://www.contributor-covenant.org/faq 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for considering contributing! 4 | 5 | This project has been developed to serve use-cases that I have encountered with 6 | uploading files to codesandbox, and definitely doesn't cover all use-cases. I'd 7 | be happy though if you find something else you need for you to contribute a pull 8 | request. I'll make sure you get a review quickly. 9 | 10 | If you're not confident raising a pull request, open an issue, and I can talk to 11 | you about the feature/bug fix you're looking for, and hopefully we can work out 12 | how to do it. 13 | 14 | ## For issues 15 | 16 | If you are reporting a bug, an attempt to understand where in our code the bug 17 | comes from, or an easy way to reproduce it is greatly appreciated! 18 | 19 | If you are requesting a feature, please make sure your use-case for the feature 20 | is clearly stated, so it's easy to evaluate. 21 | 22 | ## Deving on this project 23 | 24 | There are a couple of things that are probably good to know(TM) to dev on this 25 | project. 26 | 27 | ### How do I get up and running? 28 | 29 | We are using [bolt](https://github.com/boltpkg/bolt) to manage this monorepo. If 30 | you haven't worked on a bolt project, the quick get-up-and-running steps are: 31 | 32 | ```sh 33 | pnpm global add bolt 34 | bolt 35 | pnpm run build 36 | ``` 37 | 38 | The `bolt` command will install npm packages and link them. 39 | 40 | ### Observing changes 41 | 42 | If you are trying to observe changes across linked packages, you will need to 43 | make sure they are built. 44 | 45 | `pnpm run build` builds all packages. `pnpm run dev:csb` runs the build script 46 | for `codesandboxer` and watches it for changes. `pnpm run dev:rcsb` runs the 47 | build script for `react-codesandboxer` and watches it for changes. 48 | 49 | The other packages do not need to be built. 50 | 51 | ### Validating changes 52 | 53 | Currently validation that things work is mostly being done through tests. The 54 | most important tests being the ones in `codesandboxer` and `codesandboxer-fs` 55 | which use the `/fixtures` directory to validate how they parse and load files. 56 | 57 | ## Adding Templates 58 | 59 | Codesandboxer currently supports: 60 | 61 | - create-react-app 62 | - create-react-app-typescript 63 | - vue-cli 64 | 65 | It should be easy to add new templates. Here are the places you would need to 66 | modify: 67 | 68 | 1. Add a template file to `packages/codesandboxer/templates/`. A template should 69 | include a main file that imports from `example.js` (or the relevant 70 | filetype), as well as any other necessary files to run the sandbox. 1a. Once 71 | you have your template file, export it from 72 | `packages/codesandboxer/templates/index.js` added to the object with the name 73 | of the sandbox it is for. 74 | 2. If you want a different template to be used for `codesandboxer-fs` add a 75 | template to `packages/codesandboxer-fs/templates` in the same way. (we tend 76 | to make templates for codesandboxer-fs call out the use of the sandboxer more 77 | explicitly, as it may be less clear how to debug it) 78 | 79 | ## For Pull Requests 80 | 81 | ### Code Standards 82 | 83 | We're using flow to help keep our code neat. If you could add types to your 84 | code, that would be 😎. Code that adds tests for its use-cases is also great. 85 | 86 | ### Documentation 87 | 88 | If you add anything to the API, please update the documentation as well. (we 89 | also accept docs PRs if you see a way to improve our documentation) 90 | 91 | ### Monitoring changes 92 | 93 | We are using 94 | [build-releases](https://www.npmjs.com/package/@atlaskit/build-releases) to add 95 | intents to change, so we can make sure our packages are released at the right 96 | semantic version. The simple answer is run `pnpm run changeset` and answer the 97 | questions. If you're not certain about semantic versioning, I would recommend 98 | checking out 99 | [this documentation](https://docs.npmjs.com/about-semantic-versioning). 100 | 101 | ## Please Be Nice 102 | 103 | I'm currently working on a code of conduct, but until that's ready, I wanted to 104 | make sure that everyone felt welcome here. If you are looking to contribute, 105 | please make sure you are respectful to anyone participating on this project. 106 | -------------------------------------------------------------------------------- /FUTURE.md: -------------------------------------------------------------------------------- 1 | ## Things I am working on 2 | 3 | - [ ] Be super careful to catch all errors 4 | - [x] Rewrite readme to focus on use-cases and explaining how to implement them. 5 | - [x] Look into supporting non-`create-react-app` uploads (Having to parse 6 | non-js files will need to be a part of this) 7 | - [ ] With `codesandboxer`, refactor the shape of it. Take in a single object as 8 | argument, to make it easier to pass things around, and so we stop having 9 | the problem of ever-expanding functions. Move more properties into a 10 | generic `config` object 11 | - [x] Add error boundaries around the loaded example component, to allow a 12 | better debugging experience when something goes wrong. 13 | 14 | ## New packages to build 15 | 16 | - [ ] `codesandboxer-loader` - use `codesandboxer-fs` to create the data object 17 | to be sent to codesandbox 18 | - [ ] `gatsby-plugin-codesandboxer` - 19 | 20 | ## Supporting other sandbox types 21 | 22 | We currently support react and react-typescript sandboxes If you pass in the 23 | `template` string, you can change your sandbox kind. 24 | 25 | ## Notes made on change alongside LBatch: 26 | 27 | ``` 28 | fetchRelativePkgJSON(componentFile) 29 | compositData(entryFile, componentFile, gitConfig) 30 | 31 | fetchRelativePkgJSON(examplePath) 32 | .then(({ 33 | pkgJSONPath, 34 | }) => 35 | 36 | from componentFile, fetch pkgJSON using pkgUp 37 | fetch entryFile and its dependents 38 | 39 | config = { 40 | gitConfig: {} | fs, 41 | pkgJSON?: {} | Promise({}), // if no pkgJSON is given, pkgUp to a pkgJSON, 42 | entryFilePath?: string // if not provided sub in default file contents, 43 | } 44 | 45 | fetchFiles(examplePath, config) 46 | 47 | ensureEntryFile() => ( 48 | if 49 | ) 50 | 51 | const parsedFile = { 52 | file: { 'name/path.js': '/* */' }, 53 | internalDependencies: [], 54 | extternalDependencies: {}, 55 | } 56 | ``` 57 | 58 | ```js 59 | const rc = findRC() 60 | const pkgJSON = ensureProjectPkgJSON({ config.pkgJSON, rc.pkgJSON /*, pkgUp.pkgJSON*/ }) 61 | const parsedEntryFile = ensureEntryFile({ config.entryFile, rc.entryFile /*, defaultedEntryFileContents*/ }) 62 | const parsedExampleFile = getExampleFile(examplePath) 63 | 64 | const files = { 65 | 'example.js': { content: parsedExampleFile.contents }, 66 | 'codesandboxerEntry.js': { content: parsedEntryFile.contents }, 67 | } 68 | 69 | return assembleAllFilesAndDeps(files, { 70 | ...parsedEntryFile.externalDeps, 71 | ...parsedExampleFile.externalDeps 72 | }, [ 73 | // ...parsedEntryFile.internalDeps, 74 | ...parsedExampleFile.internalDeps, 75 | ], pkgJSON, config) 76 | ``` 77 | 78 | ``` 79 | // entryFiles cannot have internalDeps 80 | import './styles.css' 81 | import whatever from './example' 82 | ``` 83 | 84 | --- 85 | 86 | ```js 87 | files = { 88 | 'path/to/somewhere.js': { content: '/* the file contents */' }, 89 | }; 90 | 91 | deps = { 92 | package: '^1.1.0', 93 | }; 94 | ``` 95 | 96 | --- 97 | ``` 98 | createParamsConfig = { name } 99 | 100 | createParameters(files, externalDependencies, initialPKGJSON config ????) 101 | ``` 102 | 103 | ```js 104 | const sentPKGJSON = generatePkgJSON(externalDependencies, config) // ensures all packages have react and react-dom 105 | files['package.json'] = sentPKGJSON; 106 | files['index.html'] =
17 | 18 | 39 | -------------------------------------------------------------------------------- /fixtures/importResolution/vue/B.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 25 | 26 | 33 | -------------------------------------------------------------------------------- /fixtures/look-what-we-can-do/hidden-bonus.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
5 |

6 | An important feature of codesandboxer is it allows you to include other 7 | files, and you don't have to worry about including them. Things like this 8 | file. 9 |

10 |

11 | This means as long as you have an example file to begin with, it should 12 | Just Work™. 13 |

14 |
15 | ); 16 | -------------------------------------------------------------------------------- /fixtures/look-what-we-can-do/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'emotion'; 3 | 4 | import HiddenBonus from './hidden-bonus'; 5 | 6 | let showHiddenBonus = true; 7 | 8 | let box = css` 9 | border 4px solid pink; 10 | padding: 5px 30px; 11 | `; 12 | 13 | export default () => ( 14 |
15 |

Thanks for Reading!

16 |

17 | This is just a super simple example build to show you how codesandboxer 18 | works. 19 |

20 | {showHiddenBonus ? : null} 21 |
22 | ); 23 | -------------------------------------------------------------------------------- /fixtures/scoped/index.js: -------------------------------------------------------------------------------- 1 | import Foo from 'foo'; 2 | 3 | export default () => 'Applesauce alphabet soup'; 4 | -------------------------------------------------------------------------------- /fixtures/scoped/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "This file exists so we can test whether we get the correct package.json scope", 3 | "dependencies": { 4 | "foo": "0.3.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/simple.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () =>
This file exports a very simple component
; 4 | -------------------------------------------------------------------------------- /fixtures/simpleVue.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 33 | -------------------------------------------------------------------------------- /fixtures/testImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codesandbox/codesandboxer/57cafa8bda3f0f20d2b7dbae2c798350e8bd2326/fixtures/testImage.png -------------------------------------------------------------------------------- /fixtures/withAbsoluteImport.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Resolver from 'react-node-resolver'; 3 | 4 | export default () => ( 5 |
6 |

This file tests an absolute import

7 |
8 | ); 9 | -------------------------------------------------------------------------------- /fixtures/withCssImport.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import css from './importResolution/css/A.css'; 3 | 4 | export default () => ( 5 |
6 |

Simple file requiring a css file

7 |
8 | ); 9 | -------------------------------------------------------------------------------- /fixtures/withCssImportNoDeclaration.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './importResolution/css/A.css'; 3 | 4 | export default () => ( 5 |
6 |

Simple file requiring a css file

7 |
8 | ); 9 | -------------------------------------------------------------------------------- /fixtures/withJSONImport.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import JSON from './importResolution/json/A'; 3 | 4 | export default () => ( 5 |
6 |

Simple file requiring a json file

7 |
8 | ); 9 | -------------------------------------------------------------------------------- /fixtures/withPNG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import testImage from './testImage.png'; 3 | 4 | export default () => ( 5 |
6 |

Simple file requiring a png

7 |
8 | ); 9 | -------------------------------------------------------------------------------- /fixtures/withRelativeImport.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Simple from './simple'; 3 | 4 | export default () => ( 5 |
6 |

Here we are importing a simple component and rendering it:

7 | 8 |
9 | ); 10 | -------------------------------------------------------------------------------- /fixtures/withSass.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './importResolution/sass/A.sass'; 3 | 4 | export default () => ( 5 |
6 |

Simple file requiring a sass stylesheet

7 |
8 | ); 9 | -------------------------------------------------------------------------------- /fixtures/withScss.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './importResolution/scss/A.scss'; 3 | 4 | export default () => ( 5 |
6 |

Simple file requiring a scss stylesheet

7 |
8 | ); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codesandboxer-repo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/codesandbox/codesandboxer" 8 | }, 9 | "license": "MIT", 10 | "author": "Ben Conolly", 11 | "workspaces": [ 12 | "packages/*" 13 | ], 14 | "scripts": { 15 | "all-tests": "pnpm run build && pnpm run test && pnpm run flow && pnpm run lint", 16 | "build": "bolt ws run build", 17 | "changeset": "changeset", 18 | "contributors:add": "all-contributors add", 19 | "contributors:generate": "all-contributors generate", 20 | "dev:csb": "bolt w codesandboxer exec -- pnpm run build --watch", 21 | "dev:rcsb": "bolt w react-codesandboxer exec -- pnpm run build --watch", 22 | "lint": "pnpm run lint:eslint && pnpm run lint:prettier", 23 | "lint:eslint": "pnpm run eslint .", 24 | "lint:prettier": "prettier --list-different '**/*.js'", 25 | "prep-release": "changeset bump", 26 | "prettier": "prettier --write '**/*.js'", 27 | "release": "pnpm run all-tests && changeset release", 28 | "test": "pnpm run jest" 29 | }, 30 | "dependencies": { 31 | "babel-polyfill": "^6.26.0", 32 | "babel-runtime": "^6.26.0", 33 | "form-data": "^2.5.1", 34 | "isomorphic-unfetch": "^2.1.1", 35 | "lodash.isequal": "^4.5.0", 36 | "lodash.pick": "^4.4.0", 37 | "lz-string": "^1.5.0", 38 | "meow": "^9.0.0", 39 | "p-memoize": "^2.1.0", 40 | "path-browserify": "^1.0.1", 41 | "pkg-dir": "^2.0.0", 42 | "pkg-up": "^2.0.0", 43 | "prop-types": "^15.8.1", 44 | "query-string": "^6.14.1", 45 | "react-node-resolver": "^1.0.1", 46 | "react-select": "2.0.0-beta.6", 47 | "resolve": "^1.22.8" 48 | }, 49 | "devDependencies": { 50 | "@atlaskit/build-releases": "^3.0.8", 51 | "@changesets/cli": "^1.3.3", 52 | "@types/mocha": "^2.2.48", 53 | "@types/node": "^7.10.14", 54 | "all-contributors-cli": "^6.26.1", 55 | "babel-cli": "^6.26.0", 56 | "babel-core": "^6.26.3", 57 | "babel-eslint": "^8.2.6", 58 | "babel-jest": "^23.6.0", 59 | "babel-loader": "^7.1.5", 60 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 61 | "babel-plugin-transform-class-properties": "^6.24.1", 62 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 63 | "babel-plugin-transform-runtime": "^6.23.0", 64 | "babel-preset-env": "^1.7.0", 65 | "babel-preset-flow": "^6.23.0", 66 | "babel-preset-react": "^6.24.1", 67 | "bolt": "^0.23.6", 68 | "copy-webpack-plugin": "^4.6.0", 69 | "css-loader": "^0.28.11", 70 | "emotion": "9.2.12", 71 | "eslint": "^4.19.1", 72 | "eslint-plugin-react": "^7.33.2", 73 | "flow-bin": "^0.64.0", 74 | "html-webpack-plugin": "^3.2.0", 75 | "husky": "^3.1.0", 76 | "jest": "^23.6.0", 77 | "jest-in-case": "^1.0.2", 78 | "mini-css-extract-plugin": "^0.4.5", 79 | "prettier": "1.18.2", 80 | "pretty-quick": "^1.11.1", 81 | "react": "^16.14.0", 82 | "react-dom": "^16.14.0", 83 | "typescript": "^2.9.2", 84 | "vscode": "^1.1.37", 85 | "webpack": "^4.47.0", 86 | "webpack-cli": "^3.3.12", 87 | "webpack-dev-server": "^3.11.3" 88 | }, 89 | "peerDependencies": { 90 | "react": "^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0", 91 | "react-dom": "^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0" 92 | }, 93 | "bolt": { 94 | "workspaces": [ 95 | "packages/*" 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # bitbucket-releases-addon 2 | 3 | ## 1.0.5 4 | 5 | - Updated dependencies 6 | [0b60604](https://github.com/codesandbox/codesandboxer/commit/0b60604): 7 | - Updated dependencies 8 | [b46e059](https://github.com/codesandbox/codesandboxer/commit/b46e059): 9 | - codesandboxer@1.0.0 10 | 11 | ## 1.0.4 12 | 13 | - Updated dependencies [9db3c25]: 14 | - codesandboxer@0.7.0 15 | 16 | ## 1.0.3 17 | 18 | - Updated dependencies [d0c0cef]: 19 | - codesandboxer@0.6.1 20 | 21 | ## 1.0.1 22 | 23 | - [patch] Updated dependencies [becc64e](becc64e) 24 | - codesandboxer@0.5.0 25 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/bookmarklet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This bookmarklet can be run from any url that has a file open in BB or GH. Branches, commits, PR's, etc will all work. 3 | * Simply save it as a bookmark with "javascript: " and you should be good to go 4 | * To update it, use this site for now: https://mrcoles.com/bookmarklet/ 5 | */ 6 | 7 | (() => { 8 | let parseUrl = url => { 9 | let matcher; 10 | if (url.match(/^https?:\/\/bitbucket.org/)) { 11 | matcher = /^https?:\/\/(bitbucket).org\/(.+?)\/(.+?)\/src\/(.+?)\/([^?#]+)/; 12 | } else if (!!url.match(/^https?:\/\/github.com/)) { 13 | matcher = /^https?:\/\/(github).com\/(.+?)\/(.+?)\/blob\/(.+?)\/([^?#]+)/; 14 | } else { 15 | return; 16 | } 17 | const [, host, repoOwner, repoSlug, commit, file] = url.match(matcher); 18 | return { 19 | host, 20 | repoOwner, 21 | repoSlug, 22 | commit, 23 | file, 24 | }; 25 | }; 26 | let location = window.location.href; 27 | let parsed = parseUrl(location); 28 | if (!parsed) { 29 | alert('Something went wrong! We couldnt parse your url!'); 30 | } 31 | let encodedFile = encodeURIComponent(parsed.file); 32 | let url = `https://bitbucket-codesandboxer.netlify.com/deploy-file/index.html?host=${parsed.host}&repoOwner=${parsed.repoOwner}&repoSlug=${parsed.repoSlug}&commit=${parsed.commit}&file=${encodedFile}`; 33 | window.open(url, '_blank', ''); 34 | })(); 35 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bitbucket-releases-addon", 3 | "version": "1.0.5", 4 | "description": "Bitbucket addon for displaying releases section in PRs", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/codesandbox/codesandboxer", 8 | "directory": "packages/bitbucket-codesandboxer" 9 | }, 10 | "license": "Apache-2.0", 11 | "author": "Luke Batchelor", 12 | "scripts": { 13 | "build": "webpack -p", 14 | "deploy": "pnpm run build && netlify deploy", 15 | "dev": "webpack-dev-server --mode development" 16 | }, 17 | "dependencies": { 18 | "babel-polyfill": "^6.26.0", 19 | "codesandboxer": "^1.0.0", 20 | "p-memoize": "^2.0.0", 21 | "path-browserify": "^1.0.0", 22 | "prop-types": "^15.6.1", 23 | "query-string": "^6.1.0", 24 | "react": "^16.2.0", 25 | "react-dom": "^16.2.0", 26 | "react-select": "2.0.0-beta.6" 27 | }, 28 | "devDependencies": { 29 | "babel-core": "^6.26.3", 30 | "babel-loader": "^7.1.4", 31 | "babel-preset-env": "^1.6.1", 32 | "babel-preset-react": "^6.24.1", 33 | "copy-webpack-plugin": "^4.5.1", 34 | "css-loader": "^0.28.11", 35 | "mini-css-extract-plugin": "^0.4.0", 36 | "webpack": "^4.9.1", 37 | "webpack-cli": "^3.1.1", 38 | "webpack-dev-server": "^3.1.4" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/atlassian-connect.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bitbucket Codesandboxer", 3 | "description": 4 | "Addon to display a 'Deploy to Codesandbox' button in Bitbucket", 5 | "baseUrl": "https://bitbucket-codesandboxer.netlify.com/", 6 | "key": "bb-codesandboxer", 7 | "vendor": { 8 | "name": "Ben Conolly" 9 | }, 10 | "scopes": ["repository", "pullrequest"], 11 | "authentication": { 12 | "type": "none" 13 | }, 14 | "contexts": ["personal"], 15 | "modules": { 16 | "webItem": [ 17 | { 18 | "key": "bb-codesandboxer-pullrequest", 19 | "name": { 20 | "value": "Deploy to Codesandbox" 21 | }, 22 | "url": 23 | "/select-file/index.html?commit={pullrequest.source.commit.hash}&repoOwner={repository.owner.username}&repoSlug={repository.slug}", 24 | "location": "org.bitbucket.pullrequest.summary.actions", 25 | "params": { 26 | "auiIcon": "aui-iconfont-deploy" 27 | } 28 | }, 29 | { 30 | "key": "bb-codesandboxer-fileview", 31 | "location": "org.bitbucket.source.file.actions", 32 | "url": "/deploy-file/index.html?repoOwner={repository.owner.username}&repoSlug={repository.slug}&file={file.path}&host=bitbucket&commit={file.commit.hash}", 33 | "name": { 34 | "value": "Deploy to Codesandbox" 35 | }, 36 | "params": { 37 | "auiIcon": "aui-iconfont-deploy" 38 | } 39 | } 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/components/GitFileExplorer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import pMemoize from 'p-memoize'; 4 | import AsyncSelect from 'react-select/lib/Async'; 5 | import * as bitbucket from '../utils/bitbucket'; 6 | 7 | type State = { 8 | inputValue: string, 9 | }; 10 | const propTypes = { 11 | onFileSelect: PropTypes.func, 12 | repoName: PropTypes.string, 13 | repoOwner: PropTypes.string, 14 | repoRef: PropTypes.string, 15 | }; 16 | 17 | const memoizedBBCall = pMemoize(bitbucket.getBitbucketDir); 18 | 19 | function bbDirListToOptions(dirList) { 20 | return dirList.map(file => { 21 | const isDir = file.type === 'directory'; 22 | const label = isDir ? `📂 ${file.path}/` : `📄 ${file.path}`; 23 | const value = isDir ? `${file.path}/` : file.path; 24 | return { 25 | value, 26 | label, 27 | isDir, 28 | }; 29 | }); 30 | } 31 | 32 | function inputValueToDirPath(inputValue) { 33 | const re = /(\S+\/).*/; 34 | const match = inputValue.match(re); 35 | if (!match) return '/'; 36 | return match[1]; 37 | } 38 | 39 | function filterOptions(options, inputValue) { 40 | if (!inputValue) return options; 41 | return options.filter(option => option.value.startsWith(inputValue)); 42 | } 43 | 44 | export default class GitFileExplorer extends Component<*, State> { 45 | state = { 46 | inputValue: '', 47 | menuIsOpen: true, 48 | message: 'Uploading packages/core/avatar/examples/BasicAvatar.jsx', 49 | }; 50 | handleInputChange = (newValue: string, actionMeta) => { 51 | const { action } = actionMeta; 52 | if (action === 'input-change' || actionMeta.custom) { 53 | this.setState({ inputValue: newValue || '' }); 54 | return newValue; 55 | } 56 | return this.state.inputValue; 57 | }; 58 | handleSetValue = selected => { 59 | this.setState({ 60 | inputValue: selected.value, 61 | menuIsOpen: selected.isDir, 62 | message: !selected.isDir ? 'Uploading ' + selected.value : '', 63 | }); 64 | // THIS IS NAUGHTY 65 | this.selectRef.handleInputChange(selected.value, { custom: true }); 66 | if (!selected.isDir) { 67 | this.props.onFileSelect(selected.value); 68 | } 69 | }; 70 | getOptions = async inputValue => { 71 | const { repoOwner, repoName, branch } = this.props; 72 | const gitInfo = { 73 | account: repoOwner, 74 | repository: repoName, 75 | branch, 76 | }; 77 | const dirPath = inputValueToDirPath(inputValue); 78 | const dir = await memoizedBBCall(gitInfo, dirPath); 79 | const options = bbDirListToOptions(dir); 80 | return filterOptions(options, inputValue); 81 | }; 82 | render() { 83 | const { repoOwner, repoName, branch } = this.props; 84 | const linkUrl = `https://bitbucket.org/${repoOwner}/${repoName}/src/${branch}`; 85 | return ( 86 |
87 |
88 | Fetching files from: {linkUrl} 89 |
90 | { 99 | this.selectRef = ref; 100 | }} 101 | /> 102 |
{this.state.message}
103 |
104 | ); 105 | } 106 | } 107 | GitFileExplorer.propTypes = propTypes; 108 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/pages/deploy-file/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
Loading…
8 | 9 |

Loading...

10 |

11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/pages/deploy-file/index.js: -------------------------------------------------------------------------------- 1 | import queryString from 'query-string'; 2 | import * as codesandboxer from 'codesandboxer'; 3 | import loadingSpinnerStyles from './loading.css'; // eslint-disable-line 4 | import * as bitbucket from '../../utils/bitbucket'; 5 | import * as github from '../../utils/github'; 6 | 7 | const qs = queryString.parse(location.search); 8 | const spinnerEle = document.getElementById('spinner'); 9 | const statusEle = document.getElementById('status'); 10 | const subStatusEle = document.getElementById('subStatus'); 11 | 12 | function updateStatus(status = '', subStatus = '') { 13 | // We use innerHtml because we want to be able to inject an anchor 14 | statusEle.innerHTML = status; 15 | subStatusEle.innerHTML = subStatus; 16 | } 17 | 18 | function getFileUrl(csbOptions) { 19 | const { account, repository, branch } = csbOptions.gitInfo; 20 | if (csbOptions.gitInfo.host === 'bitbucket') { 21 | return `https://bitbucket.org/${account}/${repository}/src/${branch}/${csbOptions.examplePath}`; 22 | } else { 23 | return `https://github.com/${account}/${repository}/tree/${branch}/${csbOptions.examplePath}`; 24 | } 25 | } 26 | 27 | function onSuccess(sandboxUrl) { 28 | const subStatus = `You will be automatically redirected (or you can click here)`; 29 | updateStatus('Success!', subStatus); 30 | spinnerEle.innerHTML = '✓'; 31 | spinnerEle.classList.remove('spinner'); 32 | spinnerEle.classList.add('success'); 33 | } 34 | 35 | if (!qs.file || !qs.repoOwner || !qs.repoSlug || !qs.host || !qs.commit) { 36 | console.error( 37 | 'Error: expected queryString parameters for file, repoOwner, repoSlug, host and commit' 38 | ); 39 | console.error('queryString: ', qs); 40 | updateStatus('Error: Invalid queryString (see console)'); 41 | throw new Error('Invalid queryString'); 42 | } 43 | 44 | const options = { 45 | examplePath: qs.file, 46 | gitInfo: { 47 | account: qs.repoOwner, 48 | repository: qs.repoSlug, 49 | host: qs.host, 50 | branch: qs.commit, 51 | }, 52 | }; 53 | 54 | updateStatus('Uploading example', getFileUrl(options)); 55 | const provider = qs.host === 'bitbucket' ? bitbucket : github; 56 | 57 | provider 58 | .gitPkgUp(options.gitInfo, qs.file) 59 | .then(packageJsonPath => provider.getFile(options.gitInfo, packageJsonPath)) 60 | .then(pkgJSON => { 61 | updateStatus('Uploading example', 'Fetching files...'); 62 | return pkgJSON; 63 | }) 64 | .then(pkgJSON => codesandboxer.fetchFiles({ ...options, pkgJSON })) 65 | .then(files => { 66 | updateStatus('Uploading example', 'Crafting payload...'); 67 | return files; 68 | }) 69 | .then(files => codesandboxer.finaliseCSB(files)) 70 | .then(({ parameters }) => codesandboxer.sendFilesToCSB(parameters)) 71 | .then(({ sandboxUrl }) => { 72 | onSuccess(sandboxUrl); 73 | window.open(sandboxUrl, '_blank', ''); 74 | }); 75 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/pages/deploy-file/loading.css: -------------------------------------------------------------------------------- 1 | /* 2 | Styles adapted from: https://codepen.io/MattIn4D/pen/LiKFC 3 | */ 4 | /* Absolute Center Spinner */ 5 | /* .spinner { 6 | position: fixed; 7 | z-index: 999; 8 | height: 2em; 9 | width: 2em; 10 | overflow: show; 11 | margin: auto; 12 | top: 0; 13 | left: 0; 14 | bottom: 0; 15 | right: 0; 16 | } */ 17 | 18 | /* .spinner { 19 | min-width: 10px; 20 | min-height: 10px; 21 | } */ 22 | 23 | .wrapper { 24 | height: 100%; 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | flex-direction: column; 29 | } 30 | 31 | .text { 32 | margin-top: 20px; 33 | } 34 | 35 | .success { 36 | color: green; 37 | font-size: 54px; 38 | } 39 | 40 | /* :not(:required) hides these rules from IE9 and below */ 41 | .spinner:not(:required) { 42 | /* hide "spinner..." text */ 43 | font: 0/0 a; 44 | color: transparent; 45 | text-shadow: none; 46 | background-color: transparent; 47 | border: 0; 48 | } 49 | 50 | .spinner:not(:required):after { 51 | content: ''; 52 | display: block; 53 | font-size: 10px; 54 | width: 1em; 55 | height: 1em; 56 | margin-top: -0.5em; 57 | -webkit-animation: spinner 1500ms infinite linear; 58 | -moz-animation: spinner 1500ms infinite linear; 59 | -ms-animation: spinner 1500ms infinite linear; 60 | -o-animation: spinner 1500ms infinite linear; 61 | animation: spinner 1500ms infinite linear; 62 | border-radius: 0.5em; 63 | -webkit-box-shadow: rgba(0, 0, 0, 0.75) 1.5em 0 0 0, 64 | rgba(0, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) 0 1.5em 0 0, 65 | rgba(0, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(0, 0, 0, 0.5) -1.5em 0 0 0, 66 | rgba(0, 0, 0, 0.5) -1.1em -1.1em 0 0, rgba(0, 0, 0, 0.75) 0 -1.5em 0 0, 67 | rgba(0, 0, 0, 0.75) 1.1em -1.1em 0 0; 68 | box-shadow: rgba(0, 0, 0, 0.75) 1.5em 0 0 0, 69 | rgba(0, 0, 0, 0.75) 1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) 0 1.5em 0 0, 70 | rgba(0, 0, 0, 0.75) -1.1em 1.1em 0 0, rgba(0, 0, 0, 0.75) -1.5em 0 0 0, 71 | rgba(0, 0, 0, 0.75) -1.1em -1.1em 0 0, rgba(0, 0, 0, 0.75) 0 -1.5em 0 0, 72 | rgba(0, 0, 0, 0.75) 1.1em -1.1em 0 0; 73 | } 74 | 75 | /* Animation */ 76 | 77 | @-webkit-keyframes spinner { 78 | 0% { 79 | -webkit-transform: rotate(0deg); 80 | -moz-transform: rotate(0deg); 81 | -ms-transform: rotate(0deg); 82 | -o-transform: rotate(0deg); 83 | transform: rotate(0deg); 84 | } 85 | 100% { 86 | -webkit-transform: rotate(360deg); 87 | -moz-transform: rotate(360deg); 88 | -ms-transform: rotate(360deg); 89 | -o-transform: rotate(360deg); 90 | transform: rotate(360deg); 91 | } 92 | } 93 | @-moz-keyframes spinner { 94 | 0% { 95 | -webkit-transform: rotate(0deg); 96 | -moz-transform: rotate(0deg); 97 | -ms-transform: rotate(0deg); 98 | -o-transform: rotate(0deg); 99 | transform: rotate(0deg); 100 | } 101 | 100% { 102 | -webkit-transform: rotate(360deg); 103 | -moz-transform: rotate(360deg); 104 | -ms-transform: rotate(360deg); 105 | -o-transform: rotate(360deg); 106 | transform: rotate(360deg); 107 | } 108 | } 109 | @-o-keyframes spinner { 110 | 0% { 111 | -webkit-transform: rotate(0deg); 112 | -moz-transform: rotate(0deg); 113 | -ms-transform: rotate(0deg); 114 | -o-transform: rotate(0deg); 115 | transform: rotate(0deg); 116 | } 117 | 100% { 118 | -webkit-transform: rotate(360deg); 119 | -moz-transform: rotate(360deg); 120 | -ms-transform: rotate(360deg); 121 | -o-transform: rotate(360deg); 122 | transform: rotate(360deg); 123 | } 124 | } 125 | @keyframes spinner { 126 | 0% { 127 | -webkit-transform: rotate(0deg); 128 | -moz-transform: rotate(0deg); 129 | -ms-transform: rotate(0deg); 130 | -o-transform: rotate(0deg); 131 | transform: rotate(0deg); 132 | } 133 | 100% { 134 | -webkit-transform: rotate(360deg); 135 | -moz-transform: rotate(360deg); 136 | -ms-transform: rotate(360deg); 137 | -o-transform: rotate(360deg); 138 | transform: rotate(360deg); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/pages/home/home.css: -------------------------------------------------------------------------------- 1 | .page { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 100%; 7 | } 8 | 9 | .title { 10 | display: flex; 11 | flex: 1 0 auto; 12 | flex-direction: column; 13 | justify-content: center; 14 | } 15 | 16 | .bookmarklet { 17 | margin-left: 10px; 18 | } 19 | 20 | .wrapper { 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | flex: 2 0 auto; 25 | border-radius: 4px; 26 | min-width: 400px; 27 | min-height: 400px; 28 | } 29 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/pages/home/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 |
11 |
12 |

Bitbucket Codesandboxer

13 |
14 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/pages/home/index.js: -------------------------------------------------------------------------------- 1 | import './home.css'; 2 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/pages/select-file/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/pages/select-file/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import GitFileSelector from '../../components/GitFileExplorer'; 4 | import queryString from 'query-string'; 5 | 6 | import * as codesandboxer from 'codesandboxer'; 7 | import * as bitbucket from '../../utils/bitbucket'; 8 | 9 | // TODO: Add support for github here 10 | 11 | const qs = queryString.parse(location.search); 12 | let repoOwner = 'atlassian'; 13 | let repoName = 'atlaskit-mk-2'; 14 | let commit = 'master'; 15 | 16 | if (!qs.commit || !qs.repoOwner || !qs.repoSlug) { 17 | console.error( 18 | 'Error: expected queryString parameters for commit, repoOwner and repoSlug' 19 | ); 20 | console.error('queryString: ', qs); 21 | console.error('Using defaults'); 22 | } else { 23 | repoOwner = qs.repoOwner; 24 | repoName = qs.repoSlug; 25 | commit = qs.commit; 26 | } 27 | 28 | function deployExample(filePath) { 29 | const options = { 30 | examplePath: filePath, 31 | gitInfo: { 32 | account: repoOwner, 33 | repository: repoName, 34 | host: 'bitbucket', 35 | branch: commit, 36 | }, 37 | }; 38 | bitbucket 39 | .gitPkgUp(options.gitInfo, options.examplePath) 40 | .then(packageJsonPath => 41 | bitbucket.getFile(options.gitInfo, packageJsonPath) 42 | ) 43 | .then(pkgJSON => codesandboxer.fetchFiles({ ...options, pkgJSON })) 44 | .then(files => codesandboxer.finaliseCSB(files)) 45 | .then(({ parameters }) => codesandboxer.sendFilesToCSB(parameters)) 46 | .then(({ sandboxUrl }) => { 47 | window.open(sandboxUrl, '_blank', ''); 48 | }); 49 | } 50 | 51 | const outerFlexWrapper = { 52 | display: 'flex', 53 | justifyContent: 'center', 54 | alignItems: 'center', 55 | height: '100%', 56 | }; 57 | 58 | const App = () => ( 59 |
60 |
61 |

62 | Select an example file to upload 63 |

64 | 70 |
71 |
72 | ); 73 | 74 | ReactDOM.render(, document.getElementById('root')); 75 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/utils/.gitignore: -------------------------------------------------------------------------------- 1 | # This file must never be checked in 2 | github-auth.json 3 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/utils/bitbucket.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path-browserify'; 2 | 3 | function apiBase(gitInfo) { 4 | const { account, repository, branch } = gitInfo; 5 | return `https://api.bitbucket.org/2.0/repositories/${account}/${repository}/src/${branch}`; 6 | } 7 | 8 | async function paginateRequest(url) { 9 | const MAX_PAGES = 20; // This seems like a good idea? I dunno 10 | let respJson = await fetch(url).then(res => res.json()); 11 | let values = respJson.values; 12 | const pageLen = respJson.pagelen; // The number of elements we'll get per page 13 | const size = respJson.size; // The number of values once if we were to paginate all of them 14 | if (size / pageLen > MAX_PAGES) { 15 | throw new Error( 16 | 'Paginating would require more than ' + 17 | MAX_PAGES + 18 | ' pages of requests, aborting' 19 | ); 20 | } 21 | while (respJson.next) { 22 | respJson = await fetch(respJson.next).then(res => res.json()); 23 | values = values.concat(respJson.values); 24 | } 25 | return values; 26 | } 27 | 28 | function getBitbucketDir(gitInfo, dirPath) { 29 | if (dirPath === '/') dirPath = ''; 30 | const apiUrl = `${apiBase(gitInfo)}/${dirPath}`; 31 | 32 | return paginateRequest(apiUrl).then(values => { 33 | // We can clean up the data and only return what we need here 34 | return values.map(({ path: filePath, size, type }) => ({ 35 | path: filePath, 36 | size, 37 | type: type === 'commit_file' ? 'file' : 'directory', 38 | })); 39 | }); 40 | } 41 | 42 | async function gitPkgUp(gitInfo, filePath) { 43 | // Return true if a list of file paths contains a package.json file 44 | const filesContainPkgJson = files => 45 | files.some(file => path.basename(file.path) === 'package.json'); 46 | let curPath = path.dirname(filePath); 47 | let filesInDir = await getBitbucketDir(gitInfo, curPath); 48 | while (!filesContainPkgJson(filesInDir) && curPath !== '.') { 49 | curPath = path.dirname(curPath); 50 | filesInDir = await getBitbucketDir(gitInfo, curPath); 51 | } 52 | if (!filesContainPkgJson(filesInDir)) { 53 | throw new Error('Unable to find a package.json'); 54 | } 55 | return path.join(curPath, 'package.json').replace('./', ''); 56 | } 57 | 58 | async function getFile(gitInfo, filePath) { 59 | const apiUrl = `${apiBase(gitInfo)}/${filePath}`; 60 | const resp = await fetch(apiUrl); 61 | if (path.extname(filePath) === '.json') { 62 | return resp.json(); 63 | } 64 | return resp.text(); 65 | } 66 | 67 | module.exports = { 68 | getBitbucketDir, 69 | gitPkgUp, 70 | getFile, 71 | }; 72 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/src/utils/github.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | /* 3 | Note: This file is never checked into the repo, but **must** exist when building 4 | To create this file locally, go to https://github.com/settings/tokens, click "generate a new token" 5 | only give it public_repo access. Now copy that token into a file called `github-auth.json` in this directory 6 | in this format: 7 | { 8 | "access_token": "yourTokenHere" 9 | } 10 | */ 11 | import { access_token } from './github-auth'; 12 | 13 | async function getAllFilesInDir(gitInfo, filePath) { 14 | const { account, repository, branch } = gitInfo; 15 | const apiUrl = `https://api.github.com/repos/${account}/${repository}/contents/${filePath}?ref=${branch}&access_token=${access_token}`; 16 | const resp = await fetch(apiUrl).then(res => res.json()); 17 | const files = resp.map(({ path: file, type }) => ({ 18 | path: file, 19 | type, 20 | })); 21 | return files; 22 | } 23 | 24 | async function gitPkgUp(gitInfo, filePath) { 25 | // Return true if a list of file paths contains a package.json file 26 | const filesContainPkgJson = files => 27 | files.some(file => path.basename(file.path) === 'package.json'); 28 | let curPath = path.dirname(filePath); 29 | let filesInDir = await getAllFilesInDir(gitInfo, curPath); 30 | while (!filesContainPkgJson(filesInDir) && curPath !== '.') { 31 | curPath = path.dirname(curPath); 32 | filesInDir = await getAllFilesInDir(gitInfo, curPath); 33 | } 34 | if (!filesContainPkgJson(filesInDir)) { 35 | throw new Error('Unable to find a package.json'); 36 | } 37 | return path.join(curPath, 'package.json').replace('./', ''); 38 | } 39 | 40 | async function getFile(gitInfo, filePath) { 41 | const { account, repository, branch } = gitInfo; 42 | const apiUrl = `https://api.github.com/repos/${account}/${repository}/contents/${filePath}?ref=${branch}&access_token=${access_token}`; 43 | const resp = await fetch(apiUrl).then(res => res.json()); 44 | const fileStr = atob(resp.content); // content is base64 encoded 45 | if (path.extname(filePath) === '.json') { 46 | return JSON.parse(fileStr); 47 | } 48 | return fileStr; 49 | } 50 | 51 | module.exports = { 52 | getFile, 53 | gitPkgUp, 54 | }; 55 | -------------------------------------------------------------------------------- /packages/bitbucket-codesandboxer/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | 5 | module.exports = { 6 | entry: { 7 | 'babel-polyfill': 'babel-polyfill', 8 | 'deploy-file': './src/pages/deploy-file/index.js', 9 | 'select-file': './src/pages/select-file/index.js', 10 | home: './src/pages/home/index.js', 11 | }, 12 | output: { 13 | publicPath: '/', 14 | filename: '[name]/bundle.js', 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | use: { 22 | loader: 'babel-loader', 23 | }, 24 | }, 25 | { 26 | test: /\.css$/, 27 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 28 | }, 29 | ], 30 | }, 31 | plugins: [ 32 | new HtmlWebpackPlugin({ 33 | template: './src/pages/home/index.html', 34 | filename: './index.html', 35 | chunks: ['home'], 36 | }), 37 | new HtmlWebpackPlugin({ 38 | template: './src/pages/deploy-file/index.html', 39 | filename: './deploy-file/index.html', 40 | chunks: ['deploy-file'], 41 | }), 42 | new HtmlWebpackPlugin({ 43 | template: './src/pages/select-file/index.html', 44 | filename: './select-file/index.html', 45 | chunks: ['select-file'], 46 | }), 47 | new CopyWebpackPlugin([ 48 | { 49 | from: 'src/atlassian-connect.json', 50 | to: 'atlassian-connect.json', 51 | }, 52 | ]), 53 | new MiniCssExtractPlugin({ 54 | filename: '[name]/styles.css', 55 | chunkFilename: '[id].css', 56 | }), 57 | ], 58 | }; 59 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/.npmignore: -------------------------------------------------------------------------------- 1 | **/*.test.js 2 | **/*.log -------------------------------------------------------------------------------- /packages/codesandboxer-fs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.3 4 | 5 | ### Patch Changes 6 | 7 | - [patch][2c210fa](https://github.com/codesandbox/codesandboxer/commit/2c210fa): 8 | Last publish did not have correct dists - republishing 9 | 10 | ## 1.0.2 11 | 12 | ### Patch Changes 13 | 14 | - [patch][a4400e5](https://github.com/codesandbox/codesandboxer/commit/a4400e5): 15 | Update dependencies 16 | 17 | ## 1.0.1 18 | 19 | - [patch][cedfb74](https://github.com/codesandbox/codesandboxer/commit/cedfb74): 20 | - Update repository references to point to new home. 21 | 22 | ## 1.0.0 23 | 24 | - [major][b46e059](https://github.com/codesandbox/codesandboxer/commit/b46e059): 25 | - Move codesandboxer and codesandboxer-fs to first major version, as they 26 | exist in a fairly stable and used state. 27 | - Updated dependencies 28 | [0b60604](https://github.com/codesandbox/codesandboxer/commit/0b60604): 29 | - codesandboxer@1.0.0 30 | 31 | ## 0.4.7 32 | 33 | - [patch][82a4f5f](https://github.com/codesandbox/codesandboxer/commit/82a4f5f): 34 | 35 | - Fix typo that was stopping react and react-dom being ensured by the 36 | finalisation. 37 | 38 | ## 0.4.6 39 | 40 | - [patch] af01387: 41 | 42 | - Share information on the main example's filename and use this in the url 43 | 44 | ## 0.4.5 45 | 46 | - [patch] 0f3b87a: 47 | 48 | - Pass through template in correct spot 49 | 50 | ## 0.4.3 51 | 52 | - [patch] 4b2b662: 53 | 54 | - Reorganise how templates are stored This is a bunch of changes that should 55 | mostly only be relevant internally. 56 | 57 | First is that there is a `/templates` directory instead of `constants.js` to 58 | store templates in. This makes it easy to read a template an easy to see how 59 | to add a new template 60 | 61 | Secondly, while `codesandboxer-fs` still has its own templates, it inherits 62 | templates from `codesandboxer` meaning that a template can be added in one 63 | and flow down to the other. 64 | 65 | This sets up for Vue sandboxes. 66 | 67 | - [patch] 4b2b662: 68 | 69 | - Add Vue template to upload vue sandboxes In addition, codesandboxer will do 70 | its best to autodetect if it is processing a vue, react, or react-typescript 71 | sandbox, and use the preferred sandbox unless otherwise specified. 72 | 73 | - Updated dependencies [9db3c25]: 74 | - codesandboxer@0.7.0 75 | 76 | ## 0.4.2 77 | 78 | - [patch] 7a8ec34: 79 | 80 | - Fix path resolution for example path. Thanks 81 | [Gilles De Mey](https://github.com/gillesdemey) for the fix! 82 | 83 | - Updated dependencies [d0c0cef]: 84 | - codesandboxer@0.6.1 85 | 86 | ## 0.4.1 87 | 88 | - [patch] The previous version had misnamed main files for typescript. Fixing 89 | that 90 | 91 | ## 0.4.0 92 | 93 | - [patch] We relied on `meow`, but did not have a dependency on it. We now 94 | directly depend on `meow` 95 | - [minor] 🎉 ADD TYPESCRIPT SUPPORT 🎉 (comes with auto-detection of typescript 96 | examples) 97 | 98 | ## 0.3.1 99 | 100 | - [patch] Allow the loading of css files; convert the json loader to a generic 101 | raw loader [becc64e](becc64e) 102 | - [patch] Properly spread in user-provided extensions [a59ac96](a59ac96) 103 | - [patch] Add new option to pass in 'contents' instead of requiring file path 104 | [6041b10](6041b10) 105 | 106 | ## 0.3.0 107 | 108 | Using 0.4 of codesandboxer, leading to better analysis of imports/exports and 109 | support for parsing requires Add a `name` flag that allows you to set the 110 | sandbox's name Add an `allowedExtensions` flag that allows extensions to be 111 | provided (for example '.jsx') If the entry file is of a different file type, 112 | automatically add that file type to the allowed extensions. 113 | 114 | ## 0.2.1 115 | 116 | Actually fix bug with path resolution. 117 | 118 | ## 0.2.0 119 | 120 | Using v0.3 of codesandboxer - API changes that have made fs work much more 121 | nicely with codesandboxer. dry flag is now '--dry, -D' instead of '--dry, -d' 122 | (this has been made so -d can be used with dependencies) Added '--name' flag, 123 | which allows you to name your sandbox Fixed a bug with path resolution that 124 | would lead to trying to access non-existent files 125 | 126 | ## 0.1.0 127 | 128 | Fixed a pernicious bug, changed how things work, vote of confidence bump. 129 | 130 | ## 0.0.2 131 | 132 | Added documentation 133 | 134 | ## 0.0.1 135 | 136 | Initial Release. Very unstable, do not rely upon it. 137 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/README.md: -------------------------------------------------------------------------------- 1 | # codesandboxer-fs 2 | 3 | Deploy a single javascript file to CodeSandbox as a react entry point, bundling 4 | up other files you need, as well as the relevant dependencies, using 5 | `codesandboxer`'s default package under the hood to bundle files. 6 | 7 | ## Installation 8 | 9 | ``` 10 | $ pnpm global add codesandboxer-fs 11 | ``` 12 | 13 | or 14 | 15 | ``` 16 | $ npm i -g codesandboxer-fs 17 | ``` 18 | 19 | ## Base Usage 20 | 21 | ``` 22 | $ codesandboxer some/file/path.js 23 | ``` 24 | 25 | This will take this file, upload it and its dependent files to codesandbox, and 26 | assume that the file passed in is a renderable react component. It will also 27 | bundle any needed dependencies from your `package.json` 28 | 29 | The response will look something like 30 | 31 | ```json 32 | { 33 | "sandboxId": "481nzy3v84", 34 | "sandboxUrl": "https://codesandbox.io/s/481nzy3v84?module=/example" 35 | } 36 | ``` 37 | 38 | which will be printed to your console. 39 | 40 | `codesandboxer-fs` uses the context of the package the target file is from, 41 | bases its available npm dependencies on that file's `package.json`, and will not 42 | include files imported from places outside this scope. 43 | 44 | If you point at a file with an extension that is not '.js', that file type will 45 | be loaded using our '.js' logic. This is to allow extensions such as '.jsx'. 46 | 47 | ## Flags 48 | 49 | - `--dry -d` - this flag bundles the files, and prints them to the console, as 50 | well as the list of files to be sent to codesandbox. 51 | 52 | - `--name -n` - this flag names the created sandbox, making the base link more 53 | informative when shared. 54 | 55 | - allowedExtensions - this flag provides additional extensions that will be 56 | accepted. Note that the extension type of your target file is automatically 57 | added. The format is `.jsx,.ts`, a comma separated list of file types. 58 | 59 | - `--template -t` - a string of what template to use in sending files to 60 | codesandbox. Current officially supported templates are `react` and 61 | `react-typescript`. 62 | 63 | ## Why use this instead of the CodeSandbox CLI? 64 | 65 | The CodeSandbox CLI is intended to upload an entire create-react-app project, 66 | and as such is not designed to cherry-pick a file. Codesandboxer's goal is 67 | fundamentally different, in that it wants to focus on a single component, such 68 | as an example. Codesandboxer is going to be more useful if you want to share 69 | proposed changes to a component within an existing project with someone, while 70 | CodeSandbox will be better for uploading and looking at an entire website. 71 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const assembleFiles = require('./src/assembleFiles'); 3 | const assembleFilesAndPost = require('./src/assembleFilesAndPost'); 4 | 5 | /* eslint-disable-next-line */ 6 | module.exports = { assembleFilesAndPost, assembleFiles }; 7 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codesandboxer-fs", 3 | "version": "1.0.3", 4 | "description": "Deploy local files to CodeSandbox, using an example file as an entry point", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/codesandbox/codesandboxer", 8 | "directory": "packages/codesandboxer-fs" 9 | }, 10 | "license": "MIT", 11 | "author": "Ben Conolly", 12 | "main": "index.js", 13 | "bin": { 14 | "codesandboxer": "./src/cli.js" 15 | }, 16 | "scripts": { 17 | "build": "echo done" 18 | }, 19 | "dependencies": { 20 | "codesandboxer": "^1.0.3", 21 | "meow": "^9.0.0", 22 | "pkg-dir": "^2.0.0", 23 | "resolve": "^1.7.1" 24 | }, 25 | "devDependencies": { 26 | "react-node-resolver": "^1.0.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/src/assembleFiles.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const { 3 | parseFile, 4 | replaceImports, 5 | resolvePath, 6 | finaliseCSB, 7 | ensureExtensionAndTemplate, 8 | } = require('codesandboxer'); 9 | 10 | /*:: 11 | import type { Config } from './types'; 12 | */ 13 | const templates = require('./templates'); 14 | 15 | const loadFiles = require('./loadFiles'); 16 | const resolve = require('resolve'); 17 | 18 | const fs = require('fs'); 19 | const path = require('path'); 20 | const pkgDir = require('pkg-dir'); 21 | 22 | const getAbsFilePath = (relFilePath, extensions) => { 23 | try { 24 | let firstPathResolve = path.resolve(relFilePath); 25 | let a = resolve.sync(firstPathResolve, { extensions }); 26 | return a; 27 | } catch (e) { 28 | throw { 29 | key: 'noExampleFile', 30 | relFilePath, 31 | }; 32 | } 33 | }; 34 | 35 | const getPkgJSONPath = rootDir => { 36 | let fixedPath = `${rootDir}/package.json`; 37 | try { 38 | return resolve.sync(fixedPath); 39 | } catch (e) { 40 | throw { 41 | key: 'noPKGJSON', 42 | fixedPath, 43 | }; 44 | } 45 | }; 46 | 47 | async function assembleFiles(filePath /*: string */, config /*: ?Config */) { 48 | if (!config) config = {}; 49 | 50 | let extension = path.extname(filePath) || '.js'; 51 | const extensionAndTemplate = ensureExtensionAndTemplate( 52 | extension, 53 | config.extensions, 54 | config.template 55 | ); 56 | 57 | config = { ...config, ...extensionAndTemplate }; 58 | 59 | let rootDir = await pkgDir(filePath); 60 | let absFilePath = config.contents 61 | ? filePath 62 | : getAbsFilePath(filePath, config.extensions); 63 | let pkgJSONPath = getPkgJSONPath(rootDir); 64 | let relFilePath = path.relative(rootDir, filePath); 65 | 66 | // $FlowFixMe - we genuinely want dynamic requires here 67 | let pkgJSON = require(pkgJSONPath); 68 | let exampleContent = config.contents || fs.readFileSync(absFilePath, 'utf-8'); 69 | 70 | let { file, deps, internalImports } = await parseFile( 71 | exampleContent, 72 | pkgJSON 73 | ); 74 | 75 | let newFileLocation = `example`; 76 | 77 | let template = templates[config.template]; 78 | if (!template) template = templates['create-react-app']; 79 | 80 | let baseFiles = template(newFileLocation); 81 | let newFileExtension = extension || '.js'; 82 | config.fileName = newFileLocation + newFileExtension; 83 | 84 | let files = { 85 | ...baseFiles, 86 | ...{ 87 | [config.fileName]: { 88 | content: replaceImports( 89 | file, 90 | internalImports.map(m => [m, `./${resolvePath(relFilePath, m)}`]) 91 | ), 92 | }, 93 | }, 94 | }; 95 | 96 | let final = await loadFiles({ 97 | files, 98 | deps, 99 | rootDir, 100 | pkgJSON, 101 | extensions: config.extensions, 102 | internalImports: internalImports.map(m => 103 | resolvePath(path.relative(rootDir, filePath), m) 104 | ), 105 | priorPaths: [], 106 | }); 107 | 108 | if (Object.keys(final.files).length > 120) throw { key: 'tooManyModules' }; 109 | return finaliseCSB({ ...final, template: config.template }, config); 110 | } 111 | 112 | module.exports = assembleFiles; 113 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/src/assembleFilesAndPost.js: -------------------------------------------------------------------------------- 1 | const { sendFilesToCSB } = require('codesandboxer'); 2 | const assembleFiles = require('./assembleFiles'); 3 | /*:: 4 | import type { Config } from './flow-types' 5 | */ 6 | 7 | async function assembleFilesAndPost( 8 | filePath /*: string */, 9 | config /*: Config */ 10 | ) { 11 | let { parameters, fileName } = await assembleFiles(filePath, config); 12 | let csbInfo = await sendFilesToCSB(parameters, { fileName }); 13 | return csbInfo; 14 | } 15 | 16 | module.exports = assembleFilesAndPost; 17 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @flow 3 | 'use strict'; 4 | 5 | const meow = require('meow'); 6 | const assembleFiles = require('./assembleFiles'); 7 | const assembleFilesAndPost = require('./assembleFilesAndPost'); 8 | const path = require('path'); 9 | 10 | let cli = meow( 11 | ` 12 | Usage 13 | $ codesandboxer 14 | upload the file, and other files within its package, to codesandbox. 15 | 16 | Options 17 | --dry, -D Instead of deploying, display what will be deployed 18 | --name, -n Name your sandbox 19 | 20 | Unimplemented options (coming soon) 21 | --allowedExtensions List of extensions that will be treated as if they 22 | were javascript files. Most common examples are .jsx or .ts files 23 | --files, -f Provide a list of files that will be included even if they do 24 | not end up in the graph. Format: fileA.js,fileB.js,fileC.js 25 | --dependencies -d A list of dependencies to include, even if they are not 26 | mentioned 27 | 28 | Examples 29 | $ codesandboxer some/react/component.js 30 | `, 31 | { 32 | flags: { 33 | dry: { 34 | type: 'boolean', 35 | alias: 'D', 36 | }, 37 | name: { 38 | type: 'string', 39 | alias: 'n', 40 | }, 41 | allowedExtensions: { 42 | type: 'string', 43 | description: 'Pass in extensions that can be used in addition to .js', 44 | }, 45 | files: { 46 | alias: 'f', 47 | type: 'string', 48 | description: 49 | 'Provide a list of files that will be included even if they do not get imported', 50 | help: 'files is not yet implemented', 51 | }, 52 | template: { 53 | alias: 't', 54 | type: 'string', 55 | description: 56 | 'Set the CodeSandbox template to be used with the entry file', 57 | help: 'template is not get implemented', 58 | }, 59 | dependencies: { 60 | alias: 'd', 61 | type: 'string', 62 | description: 63 | 'A list of dependencies to include, even if they are not mentioned in the bundled files', 64 | help: 'dependencies is not yet implemented', 65 | }, 66 | }, 67 | } 68 | ); 69 | 70 | async function CLIStuff(cliData) { 71 | let [filePath] = cliData.input; 72 | let config = {}; 73 | 74 | if (cliData.flags.name) config.name = cliData.flags.name; 75 | if (cliData.flags.template) config.template = cliData.flags.template; 76 | if (cliData.flags.allowedExtensions) { 77 | config.extensions = cliData.flags.allowedExtensions.split(','); 78 | } 79 | 80 | if (cliData.flags.files) { 81 | return console.error( 82 | 'We have not implemented the files flag yet to allow you to pass in custom files' 83 | ); 84 | } 85 | if (cliData.flags.dependencies) { 86 | return console.error('We have not implemented the dependencies flag yet.'); 87 | } 88 | 89 | if (!filePath) { 90 | return console.error( 91 | 'No filePath was passed in. Please pass in the path to the file you want to sandbox' 92 | ); 93 | } 94 | 95 | try { 96 | if (cliData.flags.dry) { 97 | let results = await assembleFiles(filePath, config); 98 | console.log( 99 | 'dry done, here is a list of the files to be uploaded:\n', 100 | Object.keys(results.files).join('\n') 101 | ); 102 | } else { 103 | let results = await assembleFilesAndPost(filePath, { 104 | name: cliData.flags.name, 105 | ...config, 106 | }); 107 | console.log(results); 108 | } 109 | } catch (e) { 110 | switch (e.key) { 111 | case 'noPKGJSON': 112 | return console.error( 113 | `we could not resolve a package.json at ${e.fixedPath}` 114 | ); 115 | case 'noExampleFile': 116 | return console.error( 117 | `we could not resolve the example file ${filePath}\nWe tried to resolve this at: ${path.resolve( 118 | process.cwd(), 119 | e.relFilePath 120 | )}` 121 | ); 122 | case 'tooManyModules': 123 | return console.error( 124 | "The number of files this will upload to CodeSandbox is Too Damn High, and we can't do it, sorry." 125 | ); 126 | default: 127 | return console.error(e); 128 | } 129 | } 130 | } 131 | 132 | CLIStuff(cli); 133 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/src/constants.js: -------------------------------------------------------------------------------- 1 | const baseExtensions = [ 2 | '.png', 3 | '.jpeg', 4 | '.jpg', 5 | '.gif', 6 | '.bmp', 7 | '.tiff', 8 | '.json', 9 | '.js', 10 | ]; 11 | 12 | module.exports = { baseExtensions }; 13 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/src/fs.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const assembleFiles = require('./assembleFiles'); 3 | const cases = require('jest-in-case'); 4 | 5 | const baseFiles = ['package.json', 'index.html', 'example.js', 'index.js']; 6 | 7 | cases( 8 | 'load fixtures with fs', 9 | async ( 10 | { 11 | name, 12 | deps, 13 | expectedFiles = baseFiles, 14 | fileName = 'example.js', 15 | } /*: { name: string , deps?: { [string]: string }, expectedFiles?: Array, fileName?: string }*/ 16 | ) => { 17 | try { 18 | const { files, dependencies } = await assembleFiles(name); 19 | const expectedDeps = deps || { 20 | react: '^16.2.0', 21 | 'react-dom': '^16.2.0', 22 | }; 23 | 24 | expect(expectedDeps).toMatchObject(dependencies); 25 | expect(dependencies).toMatchObject(expectedDeps); 26 | expect(Object.keys(files)).toContain(fileName); 27 | expect(Object.keys(files)).toEqual(expect.arrayContaining(expectedFiles)); 28 | } catch (e) { 29 | if (e.key) { 30 | console.log('failed with known error', e); 31 | throw e; 32 | } else { 33 | throw e; 34 | } 35 | } 36 | }, 37 | // Fun note, since jest is being run from our package root, the paths here are 38 | // relative to that 39 | [ 40 | { name: 'fixtures/simple' }, 41 | { 42 | name: 'fixtures/withSass', 43 | expectedFiles: [ 44 | 'fixtures/importResolution/sass/A.sass', 45 | 'fixtures/importResolution/sass/B.sass', 46 | ], 47 | }, 48 | { 49 | name: 'fixtures/withScss', 50 | expectedFiles: [ 51 | 'fixtures/importResolution/scss/A.scss', 52 | 'fixtures/importResolution/scss/B.scss', 53 | ], 54 | }, 55 | { 56 | name: 'fixtures/withAbsoluteImport', 57 | deps: { 58 | 'react-node-resolver': '^1.0.1', 59 | resolve: '^1.7.1', 60 | 'react-dom': '^16.2.0', 61 | react: '^16.2.0', 62 | }, 63 | }, 64 | { 65 | name: 'fixtures/withRelativeImport', 66 | expectedFiles: ['fixtures/simple.js'], 67 | }, 68 | { 69 | name: 'fixtures/withPNG', 70 | expectedFiles: ['fixtures/testImage.png'], 71 | }, 72 | { 73 | name: 'fixtures/scoped/index.js', 74 | deps: { foo: '0.3.1', react: 'latest', 'react-dom': 'latest' }, 75 | }, 76 | { 77 | name: 'fixtures/importResolution/jsx/A.jsx', 78 | expectedFiles: ['example.jsx'], 79 | fileName: 'example.jsx', 80 | }, 81 | { 82 | name: 'fixtures/importResolution/jsx/B.jsx', 83 | fileName: 'example.jsx', 84 | expectedFiles: ['example.jsx', 'fixtures/importResolution/jsx/A.jsx'], 85 | }, 86 | { 87 | name: 'fixtures/withJSONImport', 88 | }, 89 | { 90 | expectedFiles: ['fixtures/importResolution/css/A.css'], 91 | name: 'fixtures/withCssImport', 92 | }, 93 | { 94 | expectedFiles: ['fixtures/importResolution/css/A.css'], 95 | name: 'fixtures/withCssImportNoDeclaration', 96 | }, 97 | { 98 | expectedFiles: ['example.vue'], 99 | deps: { vue: 'latest' }, 100 | name: 'fixtures/simpleVue.vue', 101 | fileName: 'example.vue', 102 | }, 103 | { 104 | expectedFiles: ['example.tsx'], 105 | name: 'fixtures/importResolution/tsx/A.tsx', 106 | fileName: 'example.tsx', 107 | deps: { react: '^16.2.0', 'react-dom': '^16.2.0' }, 108 | }, 109 | ] 110 | ); 111 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/src/loadFiles.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const csb = require('codesandboxer'); 3 | const loadRelativeFile = require('./loadRelativeFile'); 4 | /*:: 5 | import type { Files, Package, Dependencies } from 'codesandboxer' 6 | type LoadFileObj = { 7 | files: Files, 8 | deps: Dependencies, 9 | internalImports: Array, 10 | rootDir: string, 11 | pkgJSON: Package, 12 | extensions: Array, 13 | priorPaths: Array, 14 | }; 15 | */ 16 | 17 | let count = 1; 18 | 19 | async function loadFiles( 20 | { 21 | files, 22 | deps, 23 | internalImports, 24 | rootDir, 25 | pkgJSON, 26 | extensions, 27 | priorPaths, 28 | } /*: LoadFileObj */ 29 | ) { 30 | let newFiles = await Promise.all( 31 | internalImports.map(filePath => { 32 | return loadRelativeFile({ 33 | filePath: `./${filePath}`, 34 | pkgJSON, 35 | rootDir, 36 | extensions, 37 | }); 38 | }) 39 | ); 40 | 41 | let moreInternalImports = []; 42 | for (let f of newFiles) { 43 | files[f.filePath] = { content: f.file }; 44 | deps = Object.assign({}, deps, f.deps); 45 | f.internalImports.forEach(m => 46 | // I think this is wrong 47 | moreInternalImports.push(csb.resolvePath(f.filePath, m)) 48 | ); 49 | } 50 | 51 | if (count > 120) { 52 | throw { key: 'tooManyModules' }; 53 | } else count++; 54 | 55 | moreInternalImports = moreInternalImports.filter( 56 | mpt => !priorPaths.includes(mpt) 57 | ); 58 | if (moreInternalImports.length > 0) { 59 | let moreFiles = await loadFiles({ 60 | files, 61 | deps, 62 | rootDir, 63 | internalImports: moreInternalImports, 64 | extensions, 65 | pkgJSON, 66 | priorPaths: priorPaths.concat(internalImports), 67 | }); 68 | return { 69 | files: Object.assign({}, files, moreFiles.files), 70 | deps: Object.assign({}, deps, moreFiles.deps), 71 | }; 72 | } else { 73 | return { files, deps }; 74 | } 75 | } 76 | 77 | module.exports = loadFiles; 78 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/src/loadRelativeFile.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const csb = require('codesandboxer'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const resolve = require('resolve'); 6 | 7 | const relToRelPkgRoot = (resolvedPath, rootDir) => 8 | path.relative(rootDir, resolvedPath); 9 | 10 | async function loadJS(resolvedPath, pkgJSON, rootDir) { 11 | let content = fs.readFileSync(resolvedPath, 'utf-8'); 12 | let file = await csb.parseFile(content, pkgJSON); 13 | 14 | return Object.assign({}, file, { 15 | filePath: relToRelPkgRoot(resolvedPath, rootDir), 16 | }); 17 | } 18 | 19 | async function loadSass(resolvedPath, pkgJSON, rootDir) { 20 | let content = fs.readFileSync(resolvedPath, 'utf-8'); 21 | let file = await csb.parseSassFile(content); 22 | return Object.assign({}, file, { 23 | filePath: relToRelPkgRoot(resolvedPath, rootDir), 24 | }); 25 | } 26 | 27 | async function loadScss(resolvedPath, pkgJSON, rootDir) { 28 | let content = fs.readFileSync(resolvedPath, 'utf-8'); 29 | let file = await csb.parseScssFile(content); 30 | return Object.assign({}, file, { 31 | filePath: relToRelPkgRoot(resolvedPath, rootDir), 32 | }); 33 | } 34 | 35 | async function loadRaw(resolvedPath, rootDir) { 36 | let file = fs.readFileSync(resolvedPath, 'utf-8'); 37 | return { 38 | file, 39 | deps: {}, 40 | internalImports: [], 41 | filePath: relToRelPkgRoot(resolvedPath, rootDir), 42 | }; 43 | } 44 | /* Remove the disable once image loading has been built */ 45 | /* eslint-disable-next-line no-unused-vars */ 46 | async function loadImages(resolvedPath, rootDir) { 47 | let file = fs.readFileSync(resolvedPath); 48 | return { 49 | file: new Buffer(file).toString('base64'), 50 | deps: {}, 51 | internalImports: [], 52 | filePath: relToRelPkgRoot(resolvedPath, rootDir), 53 | }; 54 | } 55 | 56 | /*:: 57 | import type { Package } from 'codesandboxer' 58 | 59 | type LoadRelativeObj = { 60 | filePath: string, 61 | pkgJSON: Package, 62 | rootDir: string, 63 | extensions: Array, 64 | } 65 | */ 66 | 67 | async function loadRelativeFile( 68 | { filePath, pkgJSON, rootDir, extensions } /*: LoadRelativeObj */ 69 | ) { 70 | let absPath = path.resolve(rootDir, filePath); 71 | let resolvedPath = resolve.sync(absPath, { extensions }); 72 | let extension = path.extname(resolvedPath); 73 | if (!extension) { 74 | throw { key: 'fileNoExtension', path: resolvedPath }; 75 | } 76 | if (extensions.includes(extension)) { 77 | return loadJS(resolvedPath, pkgJSON, rootDir); 78 | } 79 | 80 | switch (extension) { 81 | case '.png': 82 | case '.jpeg': 83 | case '.jpg': 84 | case '.gif': 85 | case '.bmp': 86 | case '.tiff': 87 | return loadImages(resolvedPath, rootDir); 88 | case '.json': 89 | case '.css': 90 | return loadRaw(resolvedPath, rootDir); 91 | // Our scss and sass loaders currently don't resolve imports - we need to come back and update these. 92 | case '.scss': 93 | return loadScss(resolvedPath, pkgJSON, rootDir); 94 | case '.sass': 95 | return loadSass(resolvedPath, pkgJSON, rootDir); 96 | case '.js': 97 | return loadJS(resolvedPath, pkgJSON, rootDir); 98 | default: 99 | throw new Error( 100 | `unparseable filetype: ${extension} for file ${resolvedPath}` 101 | ); 102 | } 103 | } 104 | 105 | module.exports = loadRelativeFile; 106 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/src/templates/index.js: -------------------------------------------------------------------------------- 1 | const react = require('./react'); 2 | const reactTypescript = require('./react-typescript'); 3 | const vue = require('./vue'); 4 | const { templates } = require('codesandboxer'); 5 | 6 | module.exports = { 7 | ...templates, 8 | 'create-react-app': react, 9 | 'create-react-app-typescript': reactTypescript, 10 | 'vue-cli': vue, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/src/templates/react-typescript.js: -------------------------------------------------------------------------------- 1 | const getBaseFilesTS = fileName => ({ 2 | 'index.html': { 3 | content: '
', 4 | }, 5 | 'index.tsx': { 6 | content: `/** 7 | This CodeSandbox has been automatically generated using 8 | \`codesandboxer\`. If you're curious how that happened, you can 9 | check out our docs here: https://github.com/codesandbox/codesandboxer 10 | 11 | If you experience any struggles with this sandbox, please raise an issue 12 | on github. :) 13 | */ 14 | import * as React from 'react'; 15 | import * as ReactDOM from 'react-dom'; 16 | import App from './${fileName}'; 17 | 18 | ReactDOM.render( 19 | , 20 | document.getElementById('root') 21 | );`, 22 | }, 23 | }); 24 | 25 | module.exports = getBaseFilesTS; 26 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/src/templates/react.js: -------------------------------------------------------------------------------- 1 | const getBaseFiles = fileName => ({ 2 | 'index.html': { 3 | content: '
', 4 | }, 5 | 'index.js': { 6 | content: `/** 7 | This CodeSandbox has been automatically generated using 8 | \`codesandboxer\`. If you're curious how that happened, you can 9 | check out our docs here: https://github.com/codesandbox/codesandboxer 10 | 11 | If you experience any struggles with this sandbox, please raise an issue 12 | on github. :) 13 | */ 14 | import React from 'react'; 15 | import ReactDOM from 'react-dom'; 16 | import Component from './${fileName}'; 17 | 18 | ReactDOM.render( 19 |
20 |

Component imported with codesandboxer:

21 |

(It may need additional props to get it rendering well)

22 | 23 |
, 24 | document.getElementById('root') 25 | );`, 26 | }, 27 | }); 28 | 29 | module.exports = getBaseFiles; 30 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/src/templates/vue.js: -------------------------------------------------------------------------------- 1 | const getBaseFiles = fileName => ({ 2 | 'index.html': { 3 | content: '
', 4 | }, 5 | 'index.js': { 6 | content: `/** 7 | This CodeSandbox has been automatically generated using 8 | \`codesandboxer\`. If you're curious how that happened, you can 9 | check out our docs here: https://github.com/codesandbox/codesandboxer 10 | 11 | If you experience any struggles with this sandbox, please raise an issue 12 | on github. :) 13 | */ 14 | import Vue from "vue"; 15 | import Example from './${fileName}'; 16 | 17 | Vue.config.productionTip = false; 18 | 19 | /* eslint-disable no-new */ 20 | new Vue({ 21 | el: "#root", 22 | components: { Example }, 23 | template: "" 24 | }); 25 | `, 26 | }, 27 | }); 28 | 29 | module.exports = getBaseFiles; 30 | -------------------------------------------------------------------------------- /packages/codesandboxer-fs/src/types.js: -------------------------------------------------------------------------------- 1 | /*:: 2 | import type { Files, Dependencies } from 'codesandboxer'; 3 | 4 | export type Config = { 5 | name?: string, 6 | extensions?: Array, 7 | extraFiles?: Files, 8 | extraDependencies?: Dependencies, 9 | contents?: string, 10 | }; 11 | */ 12 | -------------------------------------------------------------------------------- /packages/codesandboxer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.3 4 | 5 | ### Patch Changes 6 | 7 | - [patch][2c210fa](https://github.com/codesandbox/codesandboxer/commit/2c210fa): 8 | Last publish did not have correct dists - republishing 9 | 10 | ## 1.0.2 11 | 12 | ### Patch Changes 13 | 14 | - [patch][a4400e5](https://github.com/codesandbox/codesandboxer/commit/a4400e5): 15 | Fix bitbucket API to use the 2.0 bitbucket API 16 | 17 | ## 1.0.1 18 | 19 | - [patch][cedfb74](https://github.com/codesandbox/codesandboxer/commit/cedfb74): 20 | - Update repository references to point to new home. 21 | 22 | ## 1.0.0 23 | 24 | - [minor][0b60604](https://github.com/codesandbox/codesandboxer/commit/0b60604): 25 | - Centralise decision around what template to use into one location, and allow 26 | it to be exported for downstream packages. 27 | - [major][b46e059](https://github.com/codesandbox/codesandboxer/commit/b46e059): 28 | - Move codesandboxer and codesandboxer-fs to first major version, as they 29 | exist in a fairly stable and used state. 30 | 31 | ## 0.7.2 32 | 33 | - [patch][82a4f5f](https://github.com/codesandbox/codesandboxer/commit/82a4f5f): 34 | 35 | - Fix typo that was stopping react and react-dom being ensured by the 36 | finalisation. 37 | 38 | ## 0.7.1 39 | 40 | - [patch] af01387: 41 | 42 | - Share information on the main example's filename and use this in the url 43 | 44 | ## 0.7.0 45 | 46 | - [patch] 4b2b662: 47 | 48 | - Reorganise how templates are stored This is a bunch of changes that should 49 | mostly only be relevant internally. 50 | 51 | First is that there is a `/templates` directory instead of `constants.js` to 52 | store templates in. This makes it easy to read a template an easy to see how 53 | to add a new template 54 | 55 | Secondly, while `codesandboxer-fs` still has its own templates, it inherits 56 | templates from `codesandboxer` meaning that a template can be added in one 57 | and flow down to the other. 58 | 59 | This sets up for Vue sandboxes. 60 | 61 | - [minor] 9db3c25: 62 | 63 | - template is now passed to 'finaliseCSB' as part of the first object not the 64 | second object. 65 | 66 | - [patch] 4b2b662: 67 | 68 | - Add Vue template to upload vue sandboxes In addition, codesandboxer will do 69 | its best to autodetect if it is processing a vue, react, or react-typescript 70 | sandbox, and use the preferred sandbox unless otherwise specified. 71 | 72 | ## 0.6.1 73 | 74 | - [patch] d0c0cef: 75 | 76 | - Add basic support for sass and scss being uploaded 77 | 78 | ## 0.6.0 79 | 80 | - [minor] 🎉 ADD TYPESCRIPT SUPPORT 🎉 (comes with auto-detection of typescript 81 | examples) 82 | - [patch] Use `path-browserify` for most path actions, making the code more 83 | reliable 84 | - [patch] Import statements now no longer need a variable declaration to be 85 | parsed 86 | - [BREAKING] Remove the `allowJSX` config option - this is replaced by 87 | `extensions`, an array of allowed additional extensions 88 | - the file type of the example file is automatically added, so if your example 89 | is a `.jsx` file you no longer need to pass anything in. If it is a `.ts` or 90 | `.tsx` file, it will add both extensions as allowed extensions. Overall, you 91 | probably don't need it. 92 | 93 | ## 0.5.0 94 | 95 | - [minor] Allow the loading of css files; convert the json loader to a generic 96 | raw loader [becc64e](becc64e) 97 | 98 | ## 0.4.0 99 | 100 | - Rewrote logic that handles parsing imports. As well as just being a bit more 101 | secure, it now correctly support: 102 | - require statements as well as imports 103 | - `export a from 'b'` syntax 104 | 105 | ## 0.3.0 106 | 107 | BREAKING - fetchFiles now returns just the { files, deps } object, which was 108 | previously returned as files. It no longer return parameters or name. BREAKING - 109 | finaliseCSB is now exported. The API for this function has changed dramatically 110 | as well. This function accepts the mix of files, deps, and several other passed 111 | in values, and is used to generate the parameter hash. 112 | 113 | The new workflow goes fetchFiles() -> finaliseCSB() -> sendFilesToCSB, with the 114 | ability to put your own logic in between these. 115 | 116 | FEATURE - resolvePath is now exported. This is to support codesandboxer-fs, 117 | which wants to rely upon it. 118 | 119 | ## 0.2.2 120 | 121 | Use the formData package for deploys instead of the native web formData. This is 122 | to allow codesandboxer to be node-compatible. 123 | 124 | ## 0.2.1 125 | 126 | An internal call of `fetchRelativeFile` was not being passed the new 'config' 127 | object, causing an error in file fetching. It is now being passed the correct 128 | object. 129 | 130 | ## 0.2.0 131 | 132 | Add a new argument to `fetchFiles`, and `fetchRelativeFile` that is a config 133 | object. 134 | 135 | To the config object add `allowJSX` as a property with a boolean value. 136 | 137 | Codesandboxer can now load jsx files if you opt into it. 138 | 139 | - allow loading of JSX files 140 | - Add tests using fixtures in repo to test file resolution 141 | 142 | ## 0.1.1 143 | 144 | Stop using \* import due to struggles with transform-runtime 145 | 146 | ## 0.1.0 147 | 148 | Be extracted from react-codesandboxer 149 | -------------------------------------------------------------------------------- /packages/codesandboxer/README.md: -------------------------------------------------------------------------------- 1 | # CodeSandboxer 2 | 3 | A quick loader to load an example into `codesandbox`. Takes in an entry file 4 | from github or bitbucket that loads a react component and upload it to 5 | codesandbox. 6 | 7 | All you need to provide are the repository information, the path to the example, 8 | and the path to the `package.json`. 9 | 10 | ## What it Does 11 | 12 | Codesandbox collects files starting from a single example file and uploads the 13 | bundle to codesandbox, returning you the sandbox ID that these files generate. 14 | It includes by default an index file to render your example. 15 | 16 | ## Why this is cool 17 | 18 | ### Intelligently Fetches Dependencies 19 | 20 | Using the example file and the `package.json`, dependencies that the example 21 | uses will be added to the sandbox, and everything else will be left. 22 | 23 | ### Dynamic Import Following 24 | 25 | When CodeSandbox is pointed at a file, it can resolve relative imports into that 26 | file, meaning that examples relying on utils or images will resolve correctly. 27 | 28 | ### Customisable Usages 29 | 30 | Codesandboxer is set up so if you provide your git information and a relative 31 | path to where the example is in the repository, it will take care of everything 32 | for you. Alternatively, if you have some of the content, or wish to edit it (for 33 | example replacing particular relative imports before upload) you can deeply 34 | customise the information before it is sent. 35 | 36 | You can use this purely to help format your files for codesandbox, or you can 37 | rely much more heavily on it to do work for you. 38 | 39 | ## API 40 | 41 | ### Quick Start 42 | 43 | There is an assumed workflow to codesandboxer: 44 | 45 | ```js 46 | import { 47 | fetchFiles, 48 | finaliseCSB, 49 | sendFilesToCSB 50 | } from 'codesandboxer' 51 | 52 | /* 53 | fetchedInfo is an object containing `files`, the internal exports of the target file, and `dependencies`, the external dependencies of all files. 54 | */ 55 | let fetchedInfo = await fetchFiles({ 56 | examplePath: 'fixtures/simple' 57 | gitInfo: { 58 | host: 'github', 59 | account: 'Noviny', 60 | repository: 'codesandboxer', 61 | } 62 | }) 63 | 64 | // This also returns a finalised files and finalised dependencies property, in case you want to introspect those before sending. 65 | let finalisedInformation = finaliseCSB(fetchedInfo) 66 | let csbInfo = await sendFilesToCSB(finalisedInformation.parameters) 67 | console.log('Our sandbox\'s ID:', csbInfo.sandboxId) 68 | console.log('Simple sandbox URL:', csbInfo.sandboxUrl) 69 | ``` 70 | 71 | In addition to these three main functions in the workflow, there are also 72 | several helper functions that can be used separately. We are going to look at 73 | the three main functions first, then the helper functions. 74 | 75 | ### fetchFiles() 76 | 77 | `fetchFiles` takes in the necessary information to assemble your files bundle, 78 | and returns a promise with with an object that contains: 79 | 80 | `files`: An object of the files that are to be included in the bundle, with the 81 | entry file named as 'example.js' 82 | 83 | `dependencies`: An object containing all the external dependencies that will be 84 | required from npm to assemble your package. 85 | 86 | It takes a single argument which is an object, the properties of which are 87 | detailed below. 88 | 89 | Note that only the examplePath and gitInfo are required. Everything else can be 90 | inferred. 91 | 92 | #### `examplePath`: string 93 | 94 | examplePath is always required, and is a path relative to the root of the git 95 | repository. Assuming no example is provided, this file will be fetched from 96 | github or bitbucket. 97 | 98 | #### `gitInfo`: 99 | 100 | An object containing the information to make fetch requests from github or 101 | bitbucket. There are three mandatory properties and one optional property. 102 | 103 | - account: the name of the account the repository is under 104 | - repository: the repository name 105 | - host: where your content is hosted, accepts 'bitbucket' or 'github' 106 | - branch: optionally you can define what branch to pull from (fun fact, also 107 | accepts git hashes). Defaults to master if no branch is required. 108 | 109 | This information is needed to fetch any additional files needed. 110 | 111 | #### pkgJSON 112 | 113 | This is an optional property, that can include a package.JSON's contents as an 114 | object or a string which is the path relative to the git source directory to 115 | fetch the `package.json` from your git repository. pkgJSON finally accepts a 116 | promise that can be resolve to either of these two other types. 117 | 118 | The contents of the eventual resolved `package.json` will be used to get the 119 | correct version ranges of packages your example is relying upon, and assemble a 120 | package.json for CodeSandbox to use in pulling them in. 121 | 122 | #### importReplacements: Array 123 | 124 | importReplacements are used before parsing what imports are needed by a file, to 125 | allow you to keep control of what files are uploaded. 126 | 127 | The biggest use-case of this is if you are relying on your `src/` directory, but 128 | want to use your package from npm in the uploaded example. 129 | 130 | If you pass in a path ending in a \*, it will replace all that match the start 131 | of the pattern with the new pattern. 132 | 133 | We also expose the logic that replaces imports as `replaceImports()`, in case 134 | you want to transform a file before passing it to us. 135 | 136 | #### example 137 | 138 | If you do not want the example content to be fetched (for example, you have 139 | access to the raw code, or want to transform it yourself before analysis), you 140 | can pass in the example file as raw here (just a string). You can also pass a 141 | promise that resolves to an example's file's contents. 142 | 143 | #### extensions 144 | 145 | An array of extensions that will be treated as javascript files. For example, if 146 | you pass in [`.jsx`], when loading files, we will attempt to fetch `.jsx` files 147 | as well as `.js` and `.json` files. The extension type of your example is 148 | automatically added, so if you pass in the `examplePath` `my/cool/example.jsx`, 149 | you will not need to pass in the jsx extension. 150 | 151 | If your example file is fo type `.ts` or `.tsx` both are added. 152 | 153 | ### finaliseCSB(compiledInfo, config) 154 | 155 | The FinaliseCSB function is used to generate a parameter hash of the file 156 | contents that can be sent to CodeSandbox using `sendFilesToCSB`. It takes in the 157 | result of `fetchFiles`, however is separate so you can intercept files and 158 | either examine or modify them before it is sent to codesandbox. 159 | 160 | The config object is optional, and can have any of the following properties: 161 | 162 | #### name 163 | 164 | The name for the sandbox once created. 165 | 166 | #### extraFiles 167 | 168 | Pass in files separately to fetching them. Useful to go alongisde specific 169 | replacements in importReplacements. 170 | 171 | The shape of the files object is 172 | 173 | ``` 174 | { 175 | fileName: { 176 | content: string 177 | } 178 | } 179 | ``` 180 | 181 | The filename is the absolute path where it will be created on CodeSandbox, and 182 | the content is the file's contents as a string. 183 | 184 | If a fileName exists in your provided files, it will not be fetched when it is 185 | referenced. 186 | 187 | #### extraDependencies 188 | 189 | An object with packages formatted in the same way as the dependencies in a 190 | `package.json` which will always be included in a sandbox, even if it is not 191 | found within the example's tree. 192 | 193 | ### sendFilesToCSB() 194 | 195 | Accepts the generated `parameters` from the CodeSandbox API, and posts them for 196 | you, returning a promise that resolves to an object that has both the sandbox 197 | ID, as well as the base URL to open the sandbox on the example page. 198 | 199 | ### parseFile(file, pkgJSON) 200 | 201 | `parseFile` is our internal method for finding all the import information about 202 | a file. 203 | 204 | It accepts a raw file, or a promise that resolves to a raw file, and a 205 | `package.json` as an object, or a promise that resolves to a package.json as an 206 | object. 207 | 208 | It returns an object with the shape: 209 | 210 | ``` 211 | { 212 | file: // the raw file code 213 | deps: // the external dependencies of the package with the version range from the package.json 214 | internalImports: // a list of the internal imports that the file relies upon. 215 | } 216 | ``` 217 | 218 | ### replaceImports(code, Array<[old, new]>) 219 | 220 | The internal method we use to replace imports. This takes in a raw file, and an 221 | array of imports to replace. The first item in the array is what will be 222 | replaced, and the second is what it will be replaced with. 223 | 224 | If you pass in a path ending in a \*, it will replace all that match the start 225 | of the pattern with the new pattern. 226 | 227 | ### fetchRelativeFile( path, pkg, importReplacements: Array<[string, string]>, gitInfo, config) 228 | 229 | This function takes in a path to a file relative to the git route, and along 230 | with the git information, fetches. It will also replace the imports as provided 231 | for javascript files. 232 | 233 | It return a promise with a parsed file which is an object that looks like: 234 | 235 | ``` 236 | { 237 | file: string, 238 | deps: { [string]: string }, 239 | internalImports: Array, 240 | path: // the new path that this file will be added to within codesandbox, and which other files can now use as an importReplacement, 241 | } 242 | ``` 243 | 244 | Currently the shape of the config object should be `{ allowJSX: boolean }`. If 245 | the config object is not provided, this defaults to false. 246 | 247 | ### getSandboxUrl(id, type) 248 | 249 | Passed in a sandbox id, return a url to that sandbox. Optionally takes in a type 250 | which can be used to make the url an embed url by passing in the type `'embed'`. 251 | 252 | ## Things to do better 253 | 254 | ### Support commonJS modules 255 | 256 | Currently we are scanning for import statements, and commonJS requires are not 257 | supported. 258 | 259 | ### Support beyond react 260 | 261 | The principal developer of this works in a react context, however the core good 262 | features (file fetching from relative imports, and packages, parsing all those 263 | files into a bundle CodeSandbox understands, posting to CodeSandbox) are 264 | valuable to any CodeSandbox project. 265 | 266 | If you want to use codesandboxer to upload something other than react, please 267 | get in contact with us so we can help out. 268 | 269 | ### Does not play nicely with inline webpack loaders 270 | 271 | If you are using inline webpack loaders, we don't know how to parse those. This 272 | is not on our roadmap to support. 273 | 274 | ### Designed browser-first 275 | 276 | As `codesandboxer` is designed to operate from the browser, it's not using an 277 | AST to parse the files it is reading. If you are using it in a node context, 278 | implementing this functionality using an AST generator such as babel will likely 279 | lead to safer, more precise code. 280 | -------------------------------------------------------------------------------- /packages/codesandboxer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codesandboxer", 3 | "version": "1.0.3", 4 | "description": "Fetch files from a git repository and upload them to CodeSandbox", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/codesandbox/codesandboxer", 8 | "directory": "packages/codesandboxer" 9 | }, 10 | "license": "MIT", 11 | "author": "Ben Conolly", 12 | "files": [ 13 | "dist" 14 | ], 15 | "main": "dist/index.js", 16 | "scripts": { 17 | "build": "babel src -d dist --ignore **/*.test.js" 18 | }, 19 | "dependencies": { 20 | "babel-runtime": "^6.26.0", 21 | "form-data": "^2.3.2", 22 | "isomorphic-unfetch": "^2.0.0", 23 | "lz-string": "^1.4.4", 24 | "path-browserify": "^1.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/constants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export const codesandboxURL = 4 | 'https://codesandbox.io/api/v1/sandboxes/define?query=module=/example.js'; 5 | export const codesandboxURLJSON = 6 | 'https://codesandbox.io/api/v1/sandboxes/define?json=1'; 7 | 8 | export const getSandboxUrl = ( 9 | id: string, 10 | type?: string = 's', 11 | fileName?: string = 'example' 12 | ) => `https://codesandbox.io/${type}/${id}?module=/${fileName}`; 13 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/fetchFiles/__snapshots__/fetchFiles.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`bad snapshot tests should fetch a css example from our fixtures 1`] = ` 4 | Object { 5 | "example.js": Object { 6 | "content": "import React from 'react'; 7 | import css from './fixtures/importResolution/css/A.css'; 8 | 9 | export default () => ( 10 |
11 |

Simple file requiring a css file

12 |
13 | ); 14 | ", 15 | }, 16 | "fixtures/importResolution/css/A.css": Object { 17 | "content": "div { 18 | color: red; 19 | }", 20 | }, 21 | "index.html": Object { 22 | "content": "
", 23 | }, 24 | "index.js": Object { 25 | "content": "/** 26 | This CodeSandbox has been automatically generated using 27 | \`codesandboxer\`. If you're curious how that happened, you can 28 | check out our docs here: https://github.com/codesandbox/codesandboxer 29 | 30 | If you experience any struggles with this sandbox, please raise an issue 31 | on github. :) 32 | */ 33 | import React from 'react'; 34 | import ReactDOM from 'react-dom'; 35 | import App from './example'; 36 | 37 | ReactDOM.render( 38 | , 39 | document.getElementById('root') 40 | );", 41 | }, 42 | } 43 | `; 44 | 45 | exports[`bad snapshot tests should fetch our basic fixture example 1`] = ` 46 | Object { 47 | "example.js": Object { 48 | "content": "import React from 'react'; 49 | 50 | export default () =>
This file exports a very simple component
; 51 | ", 52 | }, 53 | "index.html": Object { 54 | "content": "
", 55 | }, 56 | "index.js": Object { 57 | "content": "/** 58 | This CodeSandbox has been automatically generated using 59 | \`codesandboxer\`. If you're curious how that happened, you can 60 | check out our docs here: https://github.com/codesandbox/codesandboxer 61 | 62 | If you experience any struggles with this sandbox, please raise an issue 63 | on github. :) 64 | */ 65 | import React from 'react'; 66 | import ReactDOM from 'react-dom'; 67 | import App from './example'; 68 | 69 | ReactDOM.render( 70 | , 71 | document.getElementById('root') 72 | );", 73 | }, 74 | } 75 | `; 76 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/fetchFiles/ensureExample.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import fetchRelativeFile from '../fetchRelativeFile'; 4 | import replaceImports from '../replaceImports'; 5 | import absolutesToRelative from '../utils/absolutesToRelative'; 6 | import { parseFile } from '../parseFile'; 7 | 8 | import type { Package, GitInfo, Config, ImportReplacement } from '../types'; 9 | 10 | export default async function ensureExample( 11 | example?: string | Promise, 12 | importReplacements: Array, 13 | pkg: Package, 14 | examplePath: string, 15 | gitInfo: GitInfo, 16 | config: Config 17 | ) { 18 | if (example) { 19 | let exampleContent = await Promise.resolve(example); 20 | let content = replaceImports( 21 | exampleContent, 22 | importReplacements.map(m => [ 23 | absolutesToRelative(examplePath, m[0]), 24 | m[1], 25 | ]) 26 | ); 27 | return parseFile(content, pkg); 28 | } else { 29 | return fetchRelativeFile( 30 | examplePath, 31 | pkg, 32 | importReplacements, 33 | gitInfo, 34 | config 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/fetchFiles/ensureExtension.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import path from 'path-browserify'; 3 | 4 | export default function ensureExtension(filePath: string): string { 5 | if (!path.extname(filePath)) { 6 | return `${filePath}.js`; 7 | } else { 8 | return filePath; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/fetchFiles/ensureExtensionAndTemplate.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const ensureExtensionAndTemplate = ( 3 | extension: string, 4 | extensions: string[] = [], 5 | template?: 'create-react-app' | 'create-react-app-typescript' | 'vue-cli' 6 | ): { 7 | extensions: string[], 8 | template: 'create-react-app' | 'create-react-app-typescript' | 'vue-cli', 9 | } => { 10 | let extensionsSet = new Set(['.js', '.json', ...extensions]); 11 | extensionsSet.add(extension); 12 | 13 | if ( 14 | ['.ts', '.tsx'].includes(extension) || 15 | template === 'create-react-app-typescript' 16 | ) { 17 | if (!template) template = 'create-react-app-typescript'; 18 | extensionsSet.add('.ts'); 19 | extensionsSet.add('.tsx'); 20 | } 21 | 22 | if (extension === '.vue' || template === 'vue-cli') { 23 | if (!template) template = 'vue-cli'; 24 | extensionsSet.add('.vue'); 25 | } 26 | 27 | if (!template) { 28 | template = 'create-react-app'; 29 | } 30 | 31 | return { extensions: [...extensionsSet], template }; 32 | }; 33 | 34 | export default ensureExtensionAndTemplate; 35 | 36 | /* 37 | That other implementation: 38 | 39 | if (!config.template) { 40 | if (['.ts', '.tsx'].includes(extension)) { 41 | config.template = 'create-react-app-typescript'; 42 | } else if (extension === '.vue' && !config.template) { 43 | config.template = 'vue-cli'; 44 | } else { 45 | config.template = 'create-react-app'; 46 | } 47 | } 48 | 49 | let extensions = ['.js', '.json']; 50 | if (config.extensions) extensions = [...extensions, ...config.extensions]; 51 | if ( 52 | extension && 53 | !baseExtensions.includes(extension) && 54 | !extensions.includes(extension) 55 | ) { 56 | extensions.push(extension); 57 | } 58 | */ 59 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/fetchFiles/ensurePkgJSON.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fetchRelativeFile from '../fetchRelativeFile'; 3 | import type { Package, GitInfo, Config, ImportReplacement } from '../types'; 4 | 5 | export default async function ensurePKGJSON( 6 | maybePkg?: Package | string | Promise, 7 | importReplacements: Array, 8 | gitInfo: GitInfo, 9 | config: Config 10 | ): Promise { 11 | let pkg = await Promise.resolve(maybePkg); 12 | if (typeof pkg === 'object') { 13 | return pkg; 14 | } else if (typeof pkg === 'string') { 15 | return fetchRelativeFile( 16 | pkg, 17 | // $FlowFixMe - we know here that this will not be a js file, the only time we NEED a pkg 18 | {}, 19 | importReplacements, 20 | gitInfo 21 | ).then(({ file }) => JSON.parse(file)); 22 | } else if (!pkg) { 23 | return fetchRelativeFile( 24 | 'package.json', 25 | // $FlowFixMe - we know here that this will not be a js file, the only time we NEED a pkg 26 | {}, 27 | importReplacements, 28 | gitInfo, 29 | config 30 | ).then(({ file }) => JSON.parse(file)); 31 | } else throw new Error('could not understand passed in package.json'); 32 | } 33 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/fetchFiles/fetchFiles.test.js: -------------------------------------------------------------------------------- 1 | import fetchFiles from './index'; 2 | /* eslint-disable-next-line no-unused-vars */ 3 | import isomorphic from 'isomorphic-unfetch'; 4 | 5 | const BBInfo = { 6 | account: 'atlassian', 7 | repository: 'atlaskit-mk-2', 8 | branch: '6546190ec6d8e1e47566882177fa941bcb8bf576', 9 | host: 'bitbucket', 10 | }; 11 | 12 | const getMainObj = (userOpts = {}) => ({ 13 | examplePath: 'packages/elements/avatar/examples/01-basicAvatar.js', 14 | pkgJSON: 'packages/elements/avatar/package.json', 15 | gitInfo: BBInfo, 16 | importReplacements: [['packages/elements/avatar/src', '@atlaskit/avatar']], 17 | dependencies: { '@atlaskit/avatar': 'latest' }, 18 | providedFiles: {}, 19 | ...userOpts, 20 | }); 21 | 22 | const getSandboxerObj = (userOpts = {}) => ({ 23 | examplePath: 'fixtures/simple.js', 24 | pkgJSON: 'package.json', 25 | gitInfo: { 26 | account: 'Noviny', 27 | repository: 'codesandboxer', 28 | branch: 'master', 29 | host: 'github', 30 | }, 31 | dependencies: { '@atlaskit/avatar': 'latest' }, 32 | providedFiles: {}, 33 | ...userOpts, 34 | }); 35 | 36 | describe('bad snapshot tests', () => { 37 | it('should fetch an example from atlaskit', () => { 38 | return fetchFiles(getMainObj()).then(res => { 39 | expect(res.template).toEqual('create-react-app'); 40 | expect(Object.keys(res.deps)).toContain('@atlaskit/theme'); 41 | expect(Object.keys(res.files)).toContain( 42 | 'packages/elements/avatar/examples-util/helpers.js' 43 | ); 44 | }); 45 | }); 46 | it('should fetch our basic fixture example', () => { 47 | return fetchFiles(getSandboxerObj()).then(res => { 48 | expect(res.files).toMatchSnapshot(); 49 | }); 50 | }); 51 | it('should fetch a css example from our fixtures', () => { 52 | return fetchFiles( 53 | getSandboxerObj({ examplePath: 'fixtures/withCssImport.js' }) 54 | ).then(res => { 55 | expect(res.files).toMatchSnapshot(); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/fetchFiles/fetchInternalDependencies.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { 3 | Files, 4 | Package, 5 | GitInfo, 6 | ImportReplacement, 7 | Config, 8 | } from '../types'; 9 | 10 | import fetchRelativeFile from '../fetchRelativeFile'; 11 | import ensureExtension from './ensureExtension'; 12 | import resolvePath from '../utils/resolvePath'; 13 | 14 | export default async function fetchInternalDependencies( 15 | internalImports: Array, 16 | files: Files, 17 | pkg: Package, 18 | deps: { [string]: string }, 19 | gitInfo: GitInfo, 20 | importReplacements: Array, 21 | config: Config, 22 | accumulatedInternalDependencies: string[] = [] 23 | ) { 24 | let newFiles = await Promise.all( 25 | internalImports.map(path => 26 | fetchRelativeFile(path, pkg, importReplacements, gitInfo, config) 27 | ) 28 | ); 29 | 30 | let moreInternalImports = []; 31 | for (let f of newFiles) { 32 | files[ensureExtension(f.path)] = { content: f.file }; 33 | deps = { ...deps, ...f.deps }; 34 | f.internalImports.forEach(m => 35 | moreInternalImports.push(resolvePath(f.path, m)) 36 | ); 37 | } 38 | 39 | accumulatedInternalDependencies = accumulatedInternalDependencies.concat( 40 | internalImports 41 | ); 42 | 43 | moreInternalImports = moreInternalImports.filter( 44 | mpt => !accumulatedInternalDependencies.includes(mpt) 45 | ); 46 | 47 | if (moreInternalImports.length > 0) { 48 | let moreFiles = await fetchInternalDependencies( 49 | moreInternalImports, 50 | files, 51 | pkg, 52 | deps, 53 | gitInfo, 54 | importReplacements, 55 | config, 56 | accumulatedInternalDependencies 57 | ); 58 | return { 59 | files: { ...files, ...moreFiles.files }, 60 | deps: { ...deps, ...moreFiles.deps }, 61 | }; 62 | } else { 63 | return { files, deps }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/fetchFiles/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import resolvePath from '../utils/resolvePath'; 3 | import replaceImports from '../replaceImports'; 4 | 5 | import ensureExample from './ensureExample'; 6 | import ensurePKGJSON from './ensurePkgJSON'; 7 | import ensureExtensionAndTemplate from './ensureExtensionAndTemplate'; 8 | import fetchInternalDependencies from './fetchInternalDependencies'; 9 | import path from 'path-browserify'; 10 | 11 | import type { 12 | Package, 13 | GitInfo, 14 | Dependencies, 15 | Files, 16 | ImportReplacement, 17 | } from '../types'; 18 | import templates from '../templates'; 19 | 20 | export default async function({ 21 | examplePath, 22 | pkgJSON, 23 | gitInfo, 24 | importReplacements = [], 25 | example, 26 | extensions = [], 27 | template, 28 | }: { 29 | examplePath: string, 30 | pkgJSON?: Package | string | Promise, 31 | gitInfo: GitInfo, 32 | importReplacements?: Array, 33 | dependencies?: Dependencies, 34 | providedFiles?: Files, 35 | example?: string | Promise, 36 | name?: string, 37 | extensions: string[], 38 | template?: 'create-react-app' | 'create-react-app-typescript' | 'vue-cli', 39 | }) { 40 | let extension = path.extname(examplePath) || '.js'; 41 | let config = ensureExtensionAndTemplate(extension, extensions, template); 42 | let pkg = await ensurePKGJSON(pkgJSON, importReplacements, gitInfo, config); 43 | 44 | let { file, deps, internalImports } = await ensureExample( 45 | example, 46 | importReplacements, 47 | pkg, 48 | examplePath, 49 | gitInfo, 50 | config 51 | ); 52 | 53 | let fileName = `example${extension}`; 54 | 55 | let files = { 56 | ...templates[config.template], 57 | [fileName]: { 58 | content: replaceImports( 59 | file, 60 | internalImports.map(m => [m, `./${resolvePath(examplePath, m)}`]) 61 | ), 62 | }, 63 | }; 64 | 65 | let final = await fetchInternalDependencies( 66 | internalImports.map(m => resolvePath(examplePath, m)), 67 | files, 68 | pkg, 69 | deps, 70 | gitInfo, 71 | importReplacements, 72 | config, 73 | [examplePath] 74 | ); 75 | return { ...final, template: config.template, fileName }; 76 | } 77 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/fetchRelativeFile/fetchRelativeFile.test.js: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import fetchRelativeFile from './'; 3 | import getUrl from './getUrl'; 4 | /* eslint-disable-next-line no-unused-vars */ 5 | import isomorphic from 'isomorphic-unfetch'; 6 | import pkgJSON from '../../../../package.json'; 7 | const GHConfig = { 8 | account: 'noviny', 9 | repository: 'react-codesandboxer', 10 | branch: '8935bc99739eff1df5961a22782e7cacbb145c5a', 11 | host: 'github', 12 | }; 13 | 14 | cases( 15 | 'fetchRelativeFile()', 16 | ({ 17 | name, 18 | pkg = pkgJSON, 19 | expectedDeps = {}, 20 | expectedInternal = [], 21 | config, 22 | json, 23 | }) => { 24 | expectedDeps = { react: '^16.2.0', ...expectedDeps }; 25 | return fetchRelativeFile(name, pkg, [], GHConfig, config).then( 26 | // We are not currently testing the file's contents. Maybe we should do this 27 | ({ file, deps, internalImports }) => { 28 | if (!json) { 29 | expect(expectedDeps).toEqual(deps); 30 | expect(expectedInternal).toEqual(internalImports); 31 | } else { 32 | expect({}).toEqual(deps); 33 | expect([]).toEqual(internalImports); 34 | let contents = JSON.parse(file); 35 | expect(contents).toEqual(json); 36 | } 37 | } 38 | ); 39 | }, 40 | [ 41 | { 42 | name: 'fixtures/simple', 43 | }, 44 | { 45 | name: 'fixtures/withAbsoluteImport', 46 | expectedDeps: { 'react-node-resolver': '^1.0.1', resolve: '^1.7.1' }, 47 | }, 48 | { 49 | name: 'fixtures/withRelativeImport', 50 | expectedInternal: ['./simple'], 51 | }, 52 | { 53 | name: 'fixtures/importResolution/js/A', 54 | }, 55 | { 56 | name: 'fixtures/importResolution/json/A', 57 | json: { a: 'A.json file' }, 58 | }, 59 | { 60 | name: 'fixtures/importResolution/jsx/A', 61 | config: { extensions: ['.jsx'] }, 62 | }, 63 | { 64 | name: 'fixtures/importResolution/ts/A', 65 | config: { extensions: ['.ts'] }, 66 | }, 67 | { 68 | name: 'fixtures/importResolution/tsx/A', 69 | config: { extensions: ['.tsx'] }, 70 | }, 71 | { 72 | name: 'fixtures/importResolution/fromIndex/js', 73 | }, 74 | { 75 | name: 'fixtures/importResolution/fromIndex/js/', 76 | }, 77 | { 78 | name: 'fixtures/importResolution/fromIndex/json', 79 | json: { a: 'index.json file' }, 80 | }, 81 | { 82 | name: 'fixtures/importResolution/fromIndex/jsx', 83 | config: { extensions: ['.jsx'] }, 84 | }, 85 | ] 86 | ); 87 | 88 | cases( 89 | 'getUrl()', 90 | ({ path, expectedType }) => { 91 | const { fileType } = getUrl(path, GHConfig); 92 | expect(fileType).toBe(expectedType); 93 | }, 94 | [ 95 | { name: 'js file', path: 'something.js', expectedType: '.js' }, 96 | { name: 'png file', path: 'something.png', expectedType: '.png' }, 97 | { 98 | name: 'png file extended', 99 | path: 'abc/something.png', 100 | expectedType: '.png', 101 | }, 102 | { 103 | name: 'png file extended', 104 | path: 'abc/something.json', 105 | expectedType: '.json', 106 | }, 107 | { 108 | name: 'root package.json fetch', 109 | path: 'package.json', 110 | expectedType: '.json', 111 | }, 112 | ] 113 | ); 114 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/fetchRelativeFile/getUrl.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { GitInfo } from '../types'; 3 | import path from 'path-browserify'; 4 | 5 | const raw = { 6 | github: (filePath, { account, repository, branch = 'master' }) => 7 | `https://raw.githubusercontent.com/${account}/${repository}/${branch}/${filePath}`, 8 | bitbucket: (filePath, { account, repository, branch = 'master' }) => 9 | `https://api.bitbucket.org/2.0/repositories/${account}/${repository}/src/${branch}/${filePath}`, 10 | }; 11 | 12 | export default function getUrl( 13 | filePath: string, 14 | { host, ...urlConfig }: GitInfo 15 | ) { 16 | let getRaw = raw[host]; 17 | if (typeof getRaw !== 'function') { 18 | throw new Error(`Could not parse files from ${host}`); 19 | } 20 | 21 | let url = getRaw(filePath, urlConfig); 22 | let extName = path.extname(filePath); 23 | if (!extName) { 24 | return { fileType: '.js', url: `${url}.js` }; 25 | } else { 26 | return { fileType: extName, url }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/fetchRelativeFile/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { parseFile, parseSassFile, parseScssFile } from '../parseFile'; 3 | import replaceImports from '../replaceImports'; 4 | import absolutesToRelative from '../utils/absolutesToRelative'; 5 | import getUrl from './getUrl'; 6 | import 'isomorphic-unfetch'; 7 | import type { 8 | Package, 9 | GitInfo, 10 | ParsedFile, 11 | Config, 12 | ImportReplacement, 13 | } from '../types'; 14 | 15 | const fetchRequest = url => 16 | fetch(url).then(res => { 17 | if (res.status === 404) { 18 | throw new Error(`file not found at: ${url}`); 19 | } 20 | return res.text(); 21 | }); 22 | 23 | /* 24 | This is modified from the canvas answer here: 25 | https://stackoverflow.com/questions/6150289/how-to-convert-image-into-base64-string-using-javascript 26 | */ 27 | function fetchImage(url, path): Promise { 28 | return new Promise(resolve => { 29 | var img = new Image(); 30 | img.crossOrigin = 'Anonymous'; 31 | img.src = url; 32 | 33 | img.onload = function() { 34 | var canvas: HTMLCanvasElement = document.createElement('canvas'); 35 | var ctx = canvas.getContext('2d'); 36 | var dataURL; 37 | canvas.height = this.naturalHeight; 38 | canvas.width = this.naturalWidth; 39 | ctx.drawImage(this, 0, 0); 40 | dataURL = canvas.toDataURL(); 41 | resolve(dataURL); 42 | }; 43 | }).then(file => ({ file, deps: {}, internalImports: [], path })); 44 | } 45 | 46 | const fetchJS = (url, path, pkg, importReplacements): Promise => 47 | fetchRequest(url) 48 | .then(content => 49 | replaceImports( 50 | content, 51 | importReplacements.map(m => [absolutesToRelative(path, m[0]), m[1]]) 52 | ) 53 | ) 54 | // this is not correct 55 | .then(content => parseFile(content, pkg)) 56 | .then(file => ({ ...file, path })); 57 | 58 | const fetchSass = (url, path) => 59 | fetchRequest(url) 60 | .then(parseSassFile) 61 | .then(file => ({ ...file, path })); 62 | 63 | const fetchScss = (url, path) => 64 | fetchRequest(url) 65 | .then(parseScssFile) 66 | .then(file => ({ ...file, path })); 67 | 68 | const fetchRaw = (url, path): Promise => { 69 | return fetch(url) 70 | .then(res => { 71 | if (res.status === 404) { 72 | throw new Error(`file not found at: ${url}`); 73 | } 74 | return res.text(); 75 | }) 76 | .then(file => ({ file, deps: {}, internalImports: [], path })); 77 | }; 78 | 79 | /* 80 | resolution order: 81 | A.js 82 | A.json 83 | A.userExtension (in order provided) 84 | A/index.js 85 | A/index.json 86 | A/index.userExtension (in order provided) 87 | */ 88 | 89 | const attemptToFetch = (url, path, pkg, importReplacements, extension) => { 90 | let newPath = `${path}${extension}`; 91 | let newUrl = url.replace(/.js$/, extension); 92 | if (extension === '.json') { 93 | return fetchRaw(newUrl, newPath).catch(error => { 94 | if (error.message.includes('file not found at:')) return; 95 | else throw error; 96 | }); 97 | } 98 | return fetchJS(newUrl, newPath, pkg, importReplacements).catch(error => { 99 | if (error.message.includes('file not found at:')) return; 100 | else throw error; 101 | }); 102 | }; 103 | 104 | // Imports that are not named may be .js, .json, or /index.js. Node resolves them 105 | // in that order. 106 | async function fetchProbablyJS(url, path, pkg, importReplacements, config) { 107 | let extensions: string[] = config.extensions || []; 108 | // We add in the .js and .json extensions as the default accepted extensions 109 | extensions = ['.js', '.json', ...extensions]; 110 | extensions = [ 111 | ...extensions, 112 | // This account for when the path references an index file 113 | ...extensions.map(extension => `/index${extension}`), 114 | ]; 115 | 116 | while (extensions.length) { 117 | let extension = extensions.shift(); 118 | const data = await attemptToFetch( 119 | url, 120 | path, 121 | pkg, 122 | importReplacements, 123 | extension 124 | ); 125 | if (data) return data; 126 | } 127 | 128 | throw new Error( 129 | `file not found at: ${url}; tried extensions: ${extensions.join(', ')}` 130 | ); 131 | } 132 | 133 | let fetchFileContents = ( 134 | url, 135 | path, 136 | { fileType, pkg, importReplacements }, 137 | config 138 | ): Promise => { 139 | if (config.extensions.includes(fileType) || fileType === '.js') { 140 | return fetchProbablyJS(url, path, pkg, importReplacements, config); 141 | } 142 | 143 | switch (fileType) { 144 | case '.png': 145 | case '.jpeg': 146 | case '.jpg': 147 | case '.gif': 148 | case '.bmp': 149 | case '.tiff': 150 | return fetchImage(url, path); 151 | case '.json': 152 | case '.css': 153 | return fetchRaw(url, path); 154 | case '.scss': 155 | return fetchScss(url, path); 156 | case '.sass': 157 | return fetchSass(url, path); 158 | default: 159 | throw new Error(`unparseable filetype: ${fileType} for file ${path}`); 160 | } 161 | }; 162 | 163 | type HandleFileFetch = Promise; 164 | 165 | export default async function fetchRelativeFile( 166 | path: string, 167 | pkg: Package, 168 | importReplacements: Array, 169 | gitInfo: GitInfo, 170 | config?: Config 171 | ): HandleFileFetch { 172 | config = config || { extensions: [] }; 173 | // The new path is the file name we will provide to CodeSandbox 174 | // Get the url from the gitInfo. For JS files, we will need to add the filetype 175 | // This method needs to determine the filetype, so we return it. 176 | let { url, fileType } = getUrl(path, gitInfo); 177 | 178 | let file = await fetchFileContents( 179 | url, 180 | path, 181 | { 182 | fileType, 183 | pkg, 184 | importReplacements, 185 | }, 186 | config 187 | ); 188 | return file; 189 | } 190 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/finaliseCSB/getParameters.js: -------------------------------------------------------------------------------- 1 | import LZString from 'lz-string'; 2 | function compress(input) { 3 | return LZString.compressToBase64(input) 4 | .replace(/\+/g, '-') // Convert '+' to '-' 5 | .replace(/\//g, '_') // Convert '/' to '_' 6 | .replace(/=+$/, ''); // Remove ending '=' 7 | } 8 | function getParameters(files) { 9 | return compress(JSON.stringify(files)); 10 | } 11 | export default getParameters; 12 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/finaliseCSB/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import getParameters from './getParameters'; 3 | import type { Files, Dependencies } from '../types'; 4 | import newpkgJSON from '../templates/packagejson'; 5 | 6 | const ensureReact = deps => { 7 | if (!deps.react && !deps['react-dom']) { 8 | deps.react = 'latest'; 9 | deps['react-dom'] = 'latest'; 10 | } else if (!deps.react) { 11 | deps.react = deps['react-dom']; 12 | } else if (!deps['react-dom']) { 13 | deps['react-dom'] = deps.react; 14 | } 15 | }; 16 | 17 | const ensureVue = deps => { 18 | if (!deps.vue) { 19 | deps.vue = 'latest'; 20 | } 21 | }; 22 | 23 | export default function( 24 | { 25 | files, 26 | deps, 27 | template = 'create-react-app', 28 | }: { files: Files, deps: Dependencies, template?: string }, 29 | config: ?{ 30 | fileName?: string, 31 | extraFiles?: Files, 32 | extraDependencies?: Dependencies, 33 | name?: string, 34 | main?: string, 35 | } 36 | ) { 37 | if (!config) config = {}; 38 | let { 39 | extraFiles, 40 | extraDependencies, 41 | name, 42 | main, 43 | fileName = 'example', 44 | } = config; 45 | let dependencies = { 46 | ...deps, 47 | ...extraDependencies, 48 | }; 49 | main = 50 | !main && template === 'create-react-app-typescript' 51 | ? 'index.tsx' 52 | : 'index.js'; 53 | 54 | if ( 55 | template === 'create-react-app' || 56 | template === 'create-react-app-typescript' 57 | ) { 58 | ensureReact(dependencies); 59 | } 60 | 61 | if (template === 'vue-cli') { 62 | ensureVue(dependencies); 63 | } 64 | 65 | const finalFiles = { 66 | ...files, 67 | 'package.json': { 68 | content: newpkgJSON(dependencies, name, main), 69 | }, 70 | 'sandbox.config.json': { 71 | content: JSON.stringify({ 72 | template: template, 73 | }), 74 | }, 75 | ...extraFiles, 76 | }; 77 | const parameters = getParameters({ 78 | files: finalFiles, 79 | }); 80 | return { 81 | files: finalFiles, 82 | dependencies, 83 | parameters, 84 | fileName, 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // exposed mostly for codesandboxer-fs to use 3 | export { parseFile, parseScssFile, parseSassFile } from './parseFile'; 4 | export { default as replaceImports } from './replaceImports'; 5 | export { default as resolvePath } from './utils/resolvePath'; 6 | 7 | // intended effective API 8 | export { default as fetchFiles } from './fetchFiles'; 9 | export { 10 | default as ensureExtensionAndTemplate, 11 | } from './fetchFiles/ensureExtensionAndTemplate'; 12 | export { default as sendFilesToCSB } from './sendFilesToCSB'; 13 | export { default as finaliseCSB } from './finaliseCSB'; 14 | export { getSandboxUrl } from './constants'; 15 | export { templates } from './templates'; 16 | 17 | // I don't know why this is exposed 18 | export { default as fetchRelativeFile } from './fetchRelativeFile'; 19 | 20 | export type { 21 | GitInfo, 22 | Files, 23 | ParsedFile, 24 | parsedFileFirst, 25 | Package, 26 | Dependencies, 27 | Import, 28 | ImportReplacement, 29 | } from './types'; 30 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/parseFile/__snapshots__/parseFile.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ParseFile() just getting imports 1`] = ` 4 | Object { 5 | "deps": Object {}, 6 | "file": "\\"// @flow 7 | 8 | export { default } from './components/Avatar'; 9 | export { default as AvatarGroup } from './components/AvatarGroup'; 10 | export { default as AvatarItem } from './components/AvatarItem'; 11 | export { default as Presence } from './components/Presence'; 12 | export { default as Status } from './components/Status'; 13 | export { default as Skeleton } from './components/Skeleton'; 14 | \\"", 15 | "internalImports": Array [ 16 | "./components/Avatar", 17 | "./components/AvatarGroup", 18 | "./components/AvatarItem", 19 | "./components/Presence", 20 | "./components/Status", 21 | "./components/Skeleton", 22 | ], 23 | } 24 | `; 25 | 26 | exports[`ParseFile() simple parse 1`] = ` 27 | Object { 28 | "deps": Object { 29 | "b": "v1.0.0", 30 | }, 31 | "file": "import a from 'b'; import c from './c'", 32 | "internalImports": Array [ 33 | "./c", 34 | ], 35 | } 36 | `; 37 | 38 | exports[`ParseFile() simple parse file promise 1`] = ` 39 | Object { 40 | "deps": Object { 41 | "b": "v1.0.0", 42 | }, 43 | "file": "import a from 'b'; import c from './c'", 44 | "internalImports": Array [ 45 | "./c", 46 | ], 47 | } 48 | `; 49 | 50 | exports[`ParseFile() simple parse pkgJSON promise 1`] = ` 51 | Object { 52 | "deps": Object { 53 | "b": "v1.0.0", 54 | }, 55 | "file": "import a from 'b'; import c from './c'", 56 | "internalImports": Array [ 57 | "./c", 58 | ], 59 | } 60 | `; 61 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/parseFile/index.js: -------------------------------------------------------------------------------- 1 | export { default as parseFile } from './parseFile'; 2 | export { parseScssFile, parseSassFile } from './parseScssfile'; 3 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/parseFile/parseDeps.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Package, Import } from '../types'; 3 | 4 | const getDeps = (pkgJSON, name) => { 5 | let deps = {}; 6 | // We are deliberately putting dependencies as the last assigned as we will care 7 | // most about the versions of dependencies over other types 8 | const dependencies = { 9 | ...pkgJSON.peerDependencies, 10 | ...pkgJSON.devDependencies, 11 | ...pkgJSON.dependencies, 12 | }; 13 | 14 | for (let dependency in dependencies) { 15 | // This exists because we need to resolve dependencies when files within 16 | // the dependency are being accessed directly. This may cause sandboxes 17 | // to depend on things they are not using very occasionally. 18 | if (name.includes(dependency)) { 19 | deps[dependency] = dependencies[dependency]; 20 | } 21 | } 22 | return deps; 23 | }; 24 | 25 | const parseDeps = ( 26 | pkgJSON: Package, 27 | imports: Array 28 | ): { 29 | deps: { [string]: string }, 30 | internalImports: Array, 31 | } => { 32 | let dependencies = {}; 33 | let internalImports = []; 34 | // This is a common pattern of going over mpt of imports. Have not found a neat function extraction for it. 35 | for (let mpt of imports) { 36 | /* We are naming complete for readability, however the variable is not used. */ 37 | /* eslint-disable-next-line no-unused-vars */ 38 | if (/^\./.test(mpt)) { 39 | internalImports.push(mpt); 40 | } else { 41 | let foundDeps = getDeps(pkgJSON, mpt); 42 | if (Object.keys(foundDeps).length < 1 && mpt !== pkgJSON.name) { 43 | console.warn(`Could not find dependency version for ${mpt}`); 44 | } else { 45 | dependencies = { ...dependencies, ...foundDeps }; 46 | } 47 | } 48 | } 49 | return { deps: dependencies, internalImports }; 50 | }; 51 | 52 | export default parseDeps; 53 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/parseFile/parseFile.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import getAllImports from '../utils/getAllImports'; 3 | import parseDeps from './parseDeps'; 4 | import type { Package, parsedFileFirst } from '../types'; 5 | 6 | const parseFile = async ( 7 | file: Promise | string, 8 | pkgJSON: Promise | Package 9 | ): Promise => { 10 | let fileCode = await Promise.resolve(file); 11 | let pkgJSONContent = await Promise.resolve(pkgJSON); 12 | let imports = getAllImports(fileCode); 13 | let { deps, internalImports } = parseDeps(pkgJSONContent, imports); 14 | 15 | return { 16 | file: fileCode, 17 | deps, 18 | internalImports, 19 | }; 20 | }; 21 | 22 | export default parseFile; 23 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/parseFile/parseFile.test.js: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import parseDeps from './parseDeps'; 3 | import { parseFile } from './'; 4 | import getAllImports from '../utils/getAllImports'; 5 | 6 | const fakePKGJSON = { 7 | name: 'a', 8 | version: '0.0.1', 9 | devDependencies: { b: 'v1.0.0', d: '2.0.0', c: '2.1.0', t: '^15.7.1' }, 10 | peerDependencies: { z: 'v1.1.0', x: 'v0.3.0' }, 11 | dependencies: { x: 'v1.2.0' }, 12 | }; 13 | 14 | const codeImportTests = [ 15 | { 16 | name: 'simple import', 17 | code: "import a from 'b'", 18 | deps: { b: 'v1.0.0' }, 19 | }, 20 | { 21 | name: 'spread import', 22 | code: "import { a } from 'b'", 23 | deps: { b: 'v1.0.0' }, 24 | }, 25 | { 26 | name: 'two imports', 27 | code: "import a from 'b' import c from 'd'", 28 | deps: { b: 'v1.0.0', d: '2.0.0' }, 29 | }, 30 | { 31 | name: 'multiline imports', 32 | code: `import a from 'b' 33 | import c from 'd'`, 34 | deps: { b: 'v1.0.0', d: '2.0.0' }, 35 | }, 36 | { 37 | name: 'two spread imports', 38 | code: "import { a, b } from 'c'", 39 | deps: { c: '2.1.0' }, 40 | }, 41 | { 42 | name: 'two spread imports multiline', 43 | code: `import { 44 | a, 45 | b 46 | } from 'c'`, 47 | deps: { c: '2.1.0' }, 48 | }, 49 | { 50 | name: 'no spaces', 51 | code: "import {a} from 'b'", 52 | deps: { b: 'v1.0.0' }, 53 | }, 54 | { 55 | name: 'dev and peer deps', 56 | code: "import {a} from 't' import s from 'z' import y from 'x'", 57 | deps: { t: '^15.7.1', z: 'v1.1.0', x: 'v1.2.0' }, 58 | }, 59 | { 60 | name: 'relativeImport', 61 | code: "import {a} from './c'", 62 | internal: ['./c'], 63 | }, 64 | { 65 | name: 'when import cannot be found', 66 | code: "import something from 'unfound-dep'", 67 | // The pkgJSON main dep is always included 68 | deps: { d: '2.0.0' }, 69 | }, 70 | { 71 | name: 'import export syntax', 72 | code: "export a from 'b'", 73 | // The pkgJSON main dep is always included 74 | deps: { b: 'v1.0.0' }, 75 | }, 76 | { 77 | name: 'simple require', 78 | code: "const a = require('b')", 79 | deps: { b: 'v1.0.0' }, 80 | }, 81 | { 82 | name: 'let require', 83 | code: "let a = require('b')", 84 | deps: { b: 'v1.0.0' }, 85 | }, 86 | { 87 | name: 'var require', 88 | code: "var a = require('b')", 89 | deps: { b: 'v1.0.0' }, 90 | }, 91 | ]; 92 | 93 | cases( 94 | 'ParseFile()', 95 | async ({ file, pkgJSON }) => { 96 | let parsedFile = await parseFile(file, pkgJSON); 97 | expect(parsedFile).toMatchSnapshot(); 98 | }, 99 | [ 100 | { 101 | name: 'simple parse', 102 | file: "import a from 'b'; import c from './c'", 103 | pkgJSON: fakePKGJSON, 104 | }, 105 | { 106 | name: 'simple parse file promise', 107 | file: Promise.resolve("import a from 'b'; import c from './c'"), 108 | pkgJSON: fakePKGJSON, 109 | }, 110 | { 111 | name: 'simple parse pkgJSON promise', 112 | file: "import a from 'b'; import c from './c'", 113 | pkgJSON: Promise.resolve(fakePKGJSON), 114 | }, 115 | { 116 | name: 'just getting imports', 117 | file: `"// @flow 118 | 119 | export { default } from './components/Avatar'; 120 | export { default as AvatarGroup } from './components/AvatarGroup'; 121 | export { default as AvatarItem } from './components/AvatarItem'; 122 | export { default as Presence } from './components/Presence'; 123 | export { default as Status } from './components/Status'; 124 | export { default as Skeleton } from './components/Skeleton'; 125 | "`, 126 | pkgJSON: fakePKGJSON, 127 | }, 128 | ] 129 | ); 130 | 131 | cases( 132 | 'parseDeps()', 133 | ({ code, deps = {}, internal = [] }) => { 134 | let imports = getAllImports(code); 135 | let parsedImports = parseDeps(fakePKGJSON, imports); 136 | 137 | expect(Array.from(parsedImports.internalImports)).toMatchObject(internal); 138 | expect(parsedImports.deps).toMatchObject(deps); 139 | }, 140 | codeImportTests 141 | ); 142 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/parseFile/parseScssfile.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { parsedFileFirst } from '../types'; 3 | 4 | const matchScssString = `@import\\s*['"\`]([^'"\`]+)['"\`]\\s*;`; 5 | const matchSassString = `@import\\s*['"\`]?([^'"\`;]+)['"\`]?\\s*;`; 6 | 7 | const getAllImports = (code, matchString) => { 8 | let matcher = new RegExp(matchString); 9 | let matches = []; 10 | let m = matcher.exec(code); 11 | 12 | while (m) { 13 | if (m[1]) { 14 | matches.push(m[1]); 15 | code = code.slice(m.index + m[0].length); 16 | matcher = new RegExp(matchString); 17 | m = matcher.exec(code); 18 | } 19 | } 20 | return matches.map(match => { 21 | if (match[0] !== '.' && match[0] !== '/') return `./${match}`; 22 | return match; 23 | }); 24 | }; 25 | 26 | const parseScssFile = async ( 27 | file: Promise | string 28 | ): Promise => { 29 | let fileCode = await Promise.resolve(file); 30 | let internalImports = getAllImports(fileCode, matchScssString).map( 31 | a => `${a}.scss` 32 | ); 33 | 34 | return { 35 | file: fileCode, 36 | internalImports, 37 | deps: {}, 38 | }; 39 | }; 40 | 41 | const parseSassFile = async ( 42 | file: Promise | string 43 | ): Promise => { 44 | let fileCode = await Promise.resolve(file); 45 | let internalImports = getAllImports(fileCode, matchSassString) 46 | .concat(getAllImports(fileCode, matchScssString)) 47 | .map(a => `${a}.sass`); 48 | 49 | return { 50 | file: fileCode, 51 | internalImports, 52 | deps: {}, 53 | }; 54 | }; 55 | 56 | export { parseScssFile, parseSassFile }; 57 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/parseFile/parseScssfile.test.js: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import { parseScssFile, parseSassFile } from './parseScssfile'; 3 | 4 | const scssImportTests = [ 5 | { 6 | name: 'simple scss', 7 | code: `@import 'B';`, 8 | internal: ['./B.scss'], 9 | }, 10 | { 11 | name: 'simple path', 12 | code: `@import './B';`, 13 | internal: ['./B.scss'], 14 | }, 15 | { 16 | name: 'double quote', 17 | code: `@import "./B";`, 18 | internal: ['./B.scss'], 19 | }, 20 | { 21 | name: 'no spaces', 22 | code: `@import'./B';`, 23 | internal: ['./B.scss'], 24 | }, 25 | { 26 | name: 'many spaces', 27 | code: `@import './B' ;`, 28 | internal: ['./B.scss'], 29 | }, 30 | { 31 | name: 'line breaks', 32 | code: `@import './B' ;`, 33 | internal: ['./B.scss'], 34 | }, 35 | { 36 | name: 'with multiple imports', 37 | code: `@import './B'; @import './C';`, 38 | internal: ['./B.scss', './C.scss'], 39 | }, 40 | { 41 | name: 'with text after', 42 | code: `@import './B'; Totes invalid eh?`, 43 | internal: ['./B.scss'], 44 | }, 45 | { 46 | // This is not being resolved correctly atm and will cause errors 47 | skip: true, 48 | name: 'Currently failing', 49 | code: `@import '/B';`, 50 | internal: ['/B.scss'], 51 | }, 52 | ]; 53 | 54 | const sassImportTests = [ 55 | { 56 | name: 'sass import', 57 | code: `@import B;`, 58 | internal: ['./B.sass'], 59 | }, 60 | { 61 | name: 'sass two import', 62 | code: `@import B; @import C;`, 63 | internal: ['./B.sass', './C.sass'], 64 | }, 65 | ]; 66 | 67 | cases( 68 | 'parseScssFile()', 69 | async ({ code, internal = [] }) => { 70 | let parsedImports = await parseScssFile(code); 71 | expect(Array.from(parsedImports.internalImports)).toMatchObject(internal); 72 | }, 73 | scssImportTests 74 | ); 75 | cases( 76 | 'parseSassFile()', 77 | async ({ code, internal = [] }) => { 78 | let parsedImports = await parseSassFile(code); 79 | expect(Array.from(parsedImports.internalImports)).toMatchObject(internal); 80 | }, 81 | sassImportTests 82 | ); 83 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/replaceImports/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import replaceImport from '../utils/replaceImport'; 3 | import type { ImportReplacement } from '../types'; 4 | 5 | export default function(code: string, oldAndNew: Array) { 6 | let newCode = code; 7 | for (let mpt of oldAndNew) { 8 | let [oldSource, newSource] = mpt; 9 | 10 | newCode = replaceImport(newCode, oldSource, newSource); 11 | } 12 | return newCode; 13 | } 14 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/replaceImports/replaceImports.test.js: -------------------------------------------------------------------------------- 1 | import cases from 'jest-in-case'; 2 | import replaceImports from './'; 3 | 4 | cases( 5 | 'replaceImports()', 6 | ({ inputFile, outputFile, replaces }) => { 7 | let code = replaceImports(inputFile, replaces); 8 | expect(code).toEqual(outputFile); 9 | }, 10 | [ 11 | { 12 | name: 'replace imports', 13 | inputFile: "import a from 'b' import c from 'd'", 14 | outputFile: "import a from 'z' import c from 'arg'", 15 | replaces: [['b', 'z'], ['d', 'arg']], 16 | }, 17 | { 18 | name: 'replace imports based on pattern', 19 | inputFile: "import b from 'b'; import c from './d/somewhere'", 20 | outputFile: "import b from 'z'; import c from 'anywhere/somewhere'", 21 | replaces: [['b', 'z'], ['./d/*', 'anywhere/']], 22 | }, 23 | { 24 | name: 'replace imports that is export-y', 25 | inputFile: "export b from 'b';", 26 | outputFile: "export b from 'z';", 27 | replaces: [['b', 'z']], 28 | }, 29 | { 30 | name: 'replace require instead of import', 31 | inputFile: "const b = require('b');", 32 | outputFile: "const b = require('z');", 33 | replaces: [['b', 'z']], 34 | }, 35 | ] 36 | ); 37 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/sendFilesToCSB/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { getSandboxUrl } from '../constants'; 3 | import 'isomorphic-unfetch'; 4 | import FormData from 'form-data'; 5 | 6 | async function sendFilesToCSB( 7 | parameters: string, 8 | config?: { fileName?: string, type?: string } 9 | ): Promise<{ sandboxId: string, sandboxUrl: string }> { 10 | if (!config) config = {}; 11 | let fileName = config.fileName ? config.fileName : 'example'; 12 | let type = config.type ? config.type : 's'; 13 | let formData = new FormData(); 14 | formData.append('parameters', parameters); 15 | 16 | return fetch('https://codesandbox.io/api/v1/sandboxes/define?json=1', { 17 | method: 'post', 18 | body: formData, 19 | mode: 'cors', 20 | }) 21 | .then(response => response.json()) 22 | .then(({ errors, sandbox_id }) => { 23 | if (errors) throw errors; 24 | return { 25 | sandboxId: sandbox_id, 26 | sandboxUrl: getSandboxUrl(sandbox_id, type, fileName), 27 | }; 28 | }); 29 | } 30 | 31 | export default sendFilesToCSB; 32 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/templates/index.js: -------------------------------------------------------------------------------- 1 | import react from './react'; 2 | import reactTypescript from './react-typescript'; 3 | import vue from './vue'; 4 | 5 | export default { 6 | 'create-react-app': react, 7 | 'create-react-app-typescript': reactTypescript, 8 | 'vue-cli': vue, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/templates/packagejson.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Dependencies } from '../types'; 3 | export default ( 4 | dependencies: Dependencies, 5 | name?: string = 'codesandboxer-example', 6 | main: string = 'index.js' 7 | ) => `{ 8 | "name": "${name}", 9 | "version": "0.0.0", 10 | "description": "A simple example deployed using react-codesandboxer", 11 | "main": "${main}", 12 | "dependencies": { 13 | ${Object.keys(dependencies) 14 | .map(k => `"${k}": "${dependencies[k]}"`) 15 | .join(',\n ')} 16 | } 17 | }`; 18 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/templates/react-typescript.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'index.html': { 3 | content: '
', 4 | }, 5 | 'index.tsx': { 6 | content: `/** 7 | This CodeSandbox has been automatically generated using 8 | \`codesandboxer\`. If you're curious how that happened, you can 9 | check out our docs here: https://github.com/codesandbox/codesandboxer 10 | 11 | If you experience any struggles with this sandbox, please raise an issue 12 | on github. :) 13 | */ 14 | import * as React from 'react'; 15 | import * as ReactDOM from 'react-dom'; 16 | import App from './example'; 17 | 18 | ReactDOM.render( 19 | , 20 | document.getElementById('root') 21 | );`, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/templates/react.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'index.html': { 3 | content: '
', 4 | }, 5 | 'index.js': { 6 | content: `/** 7 | This CodeSandbox has been automatically generated using 8 | \`codesandboxer\`. If you're curious how that happened, you can 9 | check out our docs here: https://github.com/codesandbox/codesandboxer 10 | 11 | If you experience any struggles with this sandbox, please raise an issue 12 | on github. :) 13 | */ 14 | import React from 'react'; 15 | import ReactDOM from 'react-dom'; 16 | import App from './example'; 17 | 18 | ReactDOM.render( 19 | , 20 | document.getElementById('root') 21 | );`, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/templates/vue.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'index.html': { 3 | content: '
', 4 | }, 5 | 'index.js': { 6 | content: `/** 7 | This CodeSandbox has been automatically generated using 8 | \`codesandboxer\`. If you're curious how that happened, you can 9 | check out our docs here: https://github.com/codesandbox/codesandboxer 10 | 11 | If you experience any struggles with this sandbox, please raise an issue 12 | on github. :) 13 | */ 14 | import Vue from "vue"; 15 | import Example from './example'; 16 | 17 | Vue.config.productionTip = false; 18 | 19 | /* eslint-disable no-new */ 20 | new Vue({ 21 | el: "#root", 22 | components: { Example }, 23 | template: "" 24 | }); 25 | `, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export type GitInfo = { 3 | account: string, 4 | repository: string, 5 | branch?: string, 6 | host: 'bitbucket' | 'github', 7 | }; 8 | 9 | export type Files = { 10 | [string]: { 11 | content: string, 12 | }, 13 | }; 14 | 15 | export type ParsedFile = { 16 | file: string, 17 | deps: { [string]: string }, 18 | internalImports: Array, 19 | path: string, 20 | }; 21 | 22 | export type parsedFileFirst = { 23 | file: string, 24 | deps: { [string]: string }, 25 | internalImports: Array, 26 | }; 27 | 28 | export type Package = { 29 | name: string, 30 | version: string, 31 | dependencies: { 32 | [string]: string, 33 | }, 34 | devDependencies: { 35 | [string]: string, 36 | }, 37 | peerDependencies: { 38 | [string]: string, 39 | }, 40 | }; 41 | 42 | export type Dependencies = { [string]: string }; 43 | 44 | export type Config = { extensions: string[] }; 45 | 46 | export type Import = string; 47 | 48 | export type ImportReplacement = [string, string]; 49 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/utils/absolutesToRelative.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export default function absolutesToAbsolute( 4 | currentLocation: string, 5 | targetLocation: string 6 | ) { 7 | let cr = currentLocation.split('/'); 8 | let tr = targetLocation.split('/'); 9 | 10 | // remove any shared values 11 | while (tr[0] && cr[0] === tr[0]) { 12 | cr.shift(); 13 | tr.shift(); 14 | } 15 | 16 | if (cr.length < 2) { 17 | return `./${tr.join('/')}`; 18 | } 19 | 20 | cr.shift(); 21 | 22 | return `${cr.map(() => '..').join('/')}/${tr.join('/')}`; 23 | } 24 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/utils/getAllImports.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Import } from '../types'; 3 | import getRegexStr from './getRegexMatchStr'; 4 | 5 | const getAllImports = (code: string): Array => { 6 | let matcher = new RegExp(getRegexStr(), 'g'); 7 | let matches = []; 8 | let m = matcher.exec(code); 9 | 10 | while (m) { 11 | // $1 and $3 capture groups are used for 'require' statements, while $4 and $6 12 | // are used for 'import' statements 13 | if (m[1] && m[2] && m[3]) { 14 | matches.push(m[2]); 15 | } else if (m[4] && m[5] && m[6]) { 16 | matches.push(m[5]); 17 | } 18 | code = code.slice(m.index + m[0].length); 19 | matcher = new RegExp(getRegexStr(), 'g'); 20 | m = matcher.exec(code); 21 | } 22 | return matches; 23 | }; 24 | 25 | export default getAllImports; 26 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/utils/getRegexMatchStr.js: -------------------------------------------------------------------------------- 1 | /* 2 | The below implementation takes its cues from the following repo: 3 | https://github.com/mathieudutour/deps-regex 4 | 5 | We have modified it so that it also supports the syntax: 6 | 7 | export a from 'b' 8 | */ 9 | 10 | let matchingName = '\\s*(?:[\\w${},\\s*]+)\\s*'; 11 | // strings passed to matchingDeps require a trailing opening bracket. This is to 12 | // allow replaceImport to pass in partial paths, so we can match against a file 13 | // pattern. 14 | let matchAnyDep = `([^'"\`]+)(`; 15 | let matchingDeps = (deps = matchAnyDep) => `\s*['"\`])${deps}['"\`]\s*`; 16 | 17 | // $1 and $3 capture groups are used for 'require' statements, while $4 and $6 18 | // are used for 'import' statements 19 | const getRegexStr = deps => { 20 | return ( 21 | // the first half of this regex matches require statements 22 | `((?:(?:var|const|let)${matchingName}=\\s*)?require\\(${matchingDeps( 23 | deps 24 | )}\\);?)` + 25 | // the second half of this regex matches most imports 26 | `|((?:import|export)(?:(?:${matchingName}from\\s*)|\\s*)${matchingDeps( 27 | deps 28 | )};?)` 29 | ); 30 | }; 31 | 32 | export default getRegexStr; 33 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/utils/replaceImport.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import getRegexMatchStr from './getRegexMatchStr'; 3 | 4 | export default function replaceImport( 5 | code: string, 6 | oldSource: string, 7 | newSource: string 8 | ): string { 9 | let matchString = ''; 10 | 11 | if (oldSource.match(/\*$/)) { 12 | matchString = `${oldSource.replace(/\*$/, '()([^"\']*')}`; 13 | } else { 14 | matchString = `(${oldSource})(`; 15 | } 16 | 17 | let regexMatchStr = getRegexMatchStr(matchString); 18 | 19 | let newRegex = new RegExp(regexMatchStr, 'g'); 20 | // $1 and $3 capture groups are used for 'require' statements, while $4 and $6 21 | // are used for 'import' statements 22 | return code.replace(newRegex, `$1$4${newSource}$3$6`); 23 | } 24 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/utils/resolvePath.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default function resolvePath( 3 | basePath: string, 4 | relativePath: string 5 | ): string { 6 | let newSegments = basePath.split('/').filter(a => a); 7 | let relativeSegments = relativePath.split('/').filter(a => a); 8 | let segment = relativeSegments.shift(); 9 | 10 | // For our use-case, the basePath is always a file, not a directory. 11 | // This means we can do this safely. 12 | if (segment === '..' || segment === '.') newSegments.pop(); 13 | 14 | while (segment) { 15 | switch (segment) { 16 | case '.': 17 | break; 18 | case '..': 19 | if (newSegments.length < 1) { 20 | throw new Error('Trying to access a filepath outside our scope'); 21 | } 22 | newSegments.pop(); 23 | break; 24 | default: 25 | newSegments.push(segment); 26 | } 27 | segment = relativeSegments.shift(); 28 | } 29 | return newSegments.join('/'); 30 | } 31 | -------------------------------------------------------------------------------- /packages/codesandboxer/src/utils/utils.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import cases from 'jest-in-case'; 3 | import getAllImports from './getAllImports'; 4 | import resolvePath from './resolvePath'; 5 | import absolutesToRelative from './absolutesToRelative'; 6 | import path from 'path'; 7 | 8 | const codeImportTests = [ 9 | { name: 'simple import', code: "import a from 'b'", imports: ['b'] }, 10 | { name: 'spread import', code: "import { a } from 'b'", imports: ['b'] }, 11 | { 12 | name: 'two imports', 13 | code: "import a from 'b' import c from 'd'", 14 | imports: ['b', 'd'], 15 | }, 16 | { 17 | name: 'multiline imports', 18 | code: `import a from 'b' 19 | import c from 'd'`, 20 | imports: ['b', 'd'], 21 | }, 22 | { 23 | name: 'two spread imports', 24 | code: "import { a, b } from 'c'", 25 | imports: ['c'], 26 | }, 27 | { 28 | name: 'two spread imports multiline', 29 | code: `import { 30 | a, 31 | b 32 | } from 'c'`, 33 | imports: ['c'], 34 | }, 35 | { name: 'no spaces', code: "import {a} from 'b'", imports: ['b'] }, 36 | { 37 | name: 'dev and peer deps', 38 | code: "import {a} from 't' import s from 'z' import y from 'x'", 39 | imports: ['t', 'z', 'x'], 40 | }, 41 | { 42 | name: 'relativeImport', 43 | code: "import {a} from './c'", 44 | imports: ['./c'], 45 | }, 46 | { 47 | name: 'using regex pattern', 48 | code: "import a from './c/somewhere' import b from './c/anywhere'", 49 | imports: ['./c/somewhere', './c/anywhere'], 50 | }, 51 | { 52 | name: 'import then immediately export', 53 | code: "export { default } from './abc'", 54 | imports: ['./abc'], 55 | }, 56 | { 57 | name: 'import then immediately export as value', 58 | code: "export { default as something } from './abc'", 59 | imports: ['./abc'], 60 | }, 61 | { 62 | name: 'import then immediately export not default', 63 | code: "export { urd as something } from './abc' } from 'esk'", 64 | imports: ['./abc'], 65 | }, 66 | { 67 | name: 'import without variable', 68 | code: "import './abc'", 69 | imports: ['./abc'], 70 | }, 71 | { 72 | name: 'require without variable', 73 | code: "require('./abc')", 74 | imports: ['./abc'], 75 | }, 76 | // the two tests below demonstrate cases we will handle incorrectly 77 | { 78 | skip: true, 79 | name: 'call to not intended require', 80 | code: "breakingrequire('./abc')", 81 | imports: [], 82 | }, 83 | { 84 | skip: true, 85 | name: 'not intended import match', 86 | code: "simport './abc'", 87 | imports: [], 88 | }, 89 | ]; 90 | 91 | cases( 92 | 'getAllImports()', 93 | ({ code, imports }) => { 94 | let mpts = getAllImports(code); 95 | // $FlowFixMe matchObject is a fine way to compare arrays 96 | expect(mpts).toMatchObject(imports); 97 | }, 98 | codeImportTests 99 | ); 100 | 101 | cases( 102 | 'resolvePath()', 103 | ({ basePath, relativePath, name }) => { 104 | let res = resolvePath(basePath, relativePath); 105 | expect(res).toBe(name); 106 | }, 107 | [ 108 | { basePath: 'a/b/c', relativePath: '../z', name: 'a/z' }, 109 | { basePath: 'a/b/c', relativePath: './../z', name: 'a/z' }, 110 | { 111 | basePath: 'a/b/c', 112 | relativePath: '../../z/x', 113 | name: 'z/x', 114 | }, 115 | { basePath: 'a/b/c', relativePath: './z', name: 'a/b/z' }, 116 | { 117 | basePath: 'a/b/c', 118 | relativePath: 'zxy', 119 | name: 'a/b/c/zxy', 120 | }, 121 | { 122 | basePath: 'a/b/c/', 123 | relativePath: './zxy', 124 | name: 'a/b/zxy', 125 | }, 126 | { 127 | basePath: '../..', 128 | relativePath: './a', 129 | name: '../a', 130 | }, 131 | ] 132 | ); 133 | 134 | test.skip('resolve path throws when path is too long', () => { 135 | let basePath = 'a/b/c'; 136 | let relativePath = '../../../z/x'; 137 | expect(() => resolvePath(basePath, relativePath)).toThrow(); 138 | }); 139 | 140 | test.skip('resolve path throws when path is too long mk2', () => { 141 | let basePath = '..'; 142 | let relativePath = '../z'; 143 | expect(() => resolvePath(basePath, relativePath)).toThrow(); 144 | }); 145 | 146 | cases( 147 | 'absolutesToRelative different dirs', 148 | ({ currentLocation, targetLocation }) => { 149 | let actualPath = path.relative( 150 | path.dirname(currentLocation), 151 | targetLocation 152 | ); 153 | expect(absolutesToRelative(currentLocation, targetLocation)).toBe( 154 | actualPath 155 | ); 156 | }, 157 | [ 158 | { 159 | name: 'basic case', 160 | currentLocation: 'examples/somewhere.js', 161 | targetLocation: 'src', 162 | }, 163 | { 164 | name: 'more deeply nested', 165 | currentLocation: 'examples/deeper/somewhere.js', 166 | targetLocation: 'src', 167 | }, 168 | { 169 | name: 'in same subdirectory', 170 | currentLocation: 'examples/deeper/somewhere.js', 171 | targetLocation: 'examples/fork/elsewhere', 172 | }, 173 | ] 174 | ); 175 | 176 | cases( 177 | 'absolutesToRelative same dir', 178 | ({ currentLocation, targetLocation }) => { 179 | let actualPath = path.relative( 180 | path.dirname(currentLocation), 181 | targetLocation 182 | ); 183 | expect(absolutesToRelative(currentLocation, targetLocation)).toBe( 184 | `./${actualPath}` 185 | ); 186 | }, 187 | [ 188 | { 189 | name: 'in same directory', 190 | currentLocation: 'examples/deeper/somewhere.js', 191 | targetLocation: 'examples/deeper/elsewhere', 192 | }, 193 | { 194 | name: 'in subdirectory', 195 | currentLocation: 'examples/somewhere.js', 196 | targetLocation: 'examples/deeper/elsewhere', 197 | }, 198 | ] 199 | ); 200 | -------------------------------------------------------------------------------- /packages/react-codesandboxer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.1.5 4 | 5 | ### Patch Changes 6 | 7 | - [patch][2c210fa](https://github.com/codesandbox/codesandboxer/commit/2c210fa): 8 | Last publish did not have correct dists - republishing 9 | 10 | ## 3.1.4 11 | 12 | ### Patch Changes 13 | 14 | - [patch][a4400e5](https://github.com/codesandbox/codesandboxer/commit/a4400e5): 15 | Update dependencies 16 | 17 | ## 3.1.3 18 | 19 | - [patch][cedfb74](https://github.com/codesandbox/codesandboxer/commit/cedfb74): 20 | - Update repository references to point to new home. 21 | 22 | ## 3.1.2 23 | 24 | - [patch][7c16efb](https://github.com/codesandbox/codesandboxer/commit/7c16efb): 25 | - Load files again upon deploy if specific props have been changed 26 | - Updated dependencies 27 | [0b60604](https://github.com/codesandbox/codesandboxer/commit/0b60604): 28 | - Updated dependencies 29 | [b46e059](https://github.com/codesandbox/codesandboxer/commit/b46e059): 30 | - codesandboxer@1.0.0 31 | 32 | ## 3.1.1 33 | 34 | - [patch] af01387: 35 | 36 | - Handle passing through the fileName path 37 | 38 | ## 3.1.0 39 | 40 | - [minor] 4b2b662: 41 | 42 | - Add autoload option to react-codesandboxer - should only be used to 43 | automatically open iframes 44 | 45 | - Updated dependencies [9db3c25]: 46 | - codesandboxer@0.7.0 47 | 48 | ## 3.0.1 49 | 50 | - Updated dependencies [d0c0cef]: 51 | - codesandboxer@0.6.1 52 | 53 | ## 3.0.0 54 | 55 | - [minor] 🎉 ADD TYPESCRIPT SUPPORT 🎉 (comes with auto-detection of typescript 56 | examples) 57 | - [BREAKING] remove the `allowJSX` prop, replacing it with the `extensions` 58 | prop. 59 | - [minor] Add `template` prop, so you can directly set the CodeSandbox template 60 | to use in uploading. 61 | 62 | ## 2.1.4 63 | 64 | - [patch] When there is an error in assembling a sandbox, do not try and deploy 65 | the sandbox anyway [e20b3c0](e20b3c0) 66 | - [patch] Updated dependencies [becc64e](becc64e) 67 | - codesandboxer@0.5.0 68 | 69 | ## 2.1.3 70 | 71 | Update codesandboxer version 72 | 73 | ## 2.1.2 74 | 75 | Update codesandboxer version so the changes in 2.1.1 do not break your app 76 | 77 | ## 2.1.1 78 | 79 | Update version of codesandboxer This mostly incorporates API changes with 80 | codesandboxer that should not impact those using react-coesandboxer 81 | 82 | ## 2.1.0 83 | 84 | Add new prop `afterDeployError`, to allow responding to errors. Add more robust 85 | erroring, so people are less lost when something goes wrong. 86 | 87 | ## 2.0.3 88 | 89 | Update of codesandboxer after a bug in jsx component reading. 90 | 91 | ## 2.0.2 92 | 93 | Add flag to allow the loading of jsx components. 94 | 95 | ## 2.0.1 96 | 97 | Update codesandboxer dependency to resolve bug 98 | 99 | ## 2.0.0 100 | 101 | Big change here is that, with the release of `codesandboxer`, 102 | `react-codesandboxer` is no longer personally carrying the file-fetching and 103 | deploying logic. This has been done to make codesandboxer more useful in more 104 | contexts, and so it can eventually support non-react sandboxes. 105 | 106 | This also means we are using fetch instead of form submission, which means that 107 | we can return you a sandbox ID and url to your rendered child. This means you 108 | can choose how to open your sandbox, and makes it easy to open an example in an 109 | embed instead of on CodeSandbox itself. 110 | 111 | There's also a small breaking change to handle a bug. 112 | 113 | - Breaking: dependencies that cannot be found in the passed in package.json will 114 | now no longer be added to the sandbox dependencies at 'latest'. This solves a 115 | bug where packages that were reaching into a file would have all those 116 | reach-ins added as dependencies. 117 | - Breaking: `SkipDeploy` has been renamed to `SkipRedirect` to more accurately 118 | support its role, and make the embed process naming make sense. 119 | - Breaking: Most logic has been pushed into `codesandboxer`, a standalone 120 | package to allow the complex logic in that to exist outside a single react 121 | component. 122 | 123 | ## 1.0.0 124 | 125 | This is not hugely different from 0.4.2, with most changes being to 126 | documentation, mostly moving it in to v1. That said, there are some nice quality 127 | of life improvements: 128 | 129 | - Add `name` prop to set the name value of the sandbox. 130 | - Example file is now open by default instead of index. 131 | - Update package.json and index.js templates for clarity 132 | - Edit pass on the documentation 133 | 134 | ## 0.4.2 135 | 136 | - Update our import capture function to recognise 137 | `export { default } from 'somewhere'` as a valid import. 138 | - handle multiple spaces and space-types in import capturing and comparing. 139 | 140 | ## 0.4.1 141 | 142 | - Imports of `place` resolving to `place/index.js` were being added to 143 | CodeSandbox as `place.js`. They now get their correct path. 144 | 145 | ## v0.4.0 146 | 147 | - Added changelog 148 | - Add preload prop, allowing fetching of content to happen before it is used 149 | - For imports without file extensions, attempt `.js`, `.json`, then `/index.js`, 150 | in node resolution order, instead of just failing 151 | - child render function now returns `isDeploying` and `error`, no longer returns 152 | `files`. 153 | - `onLoadComplete()` prop was added. It is called with files and parameters in 154 | an object. 155 | - `afterDeploy()` is no longer called with the parameters and files. Use 156 | onLoadComplete to hook into this information. 157 | -------------------------------------------------------------------------------- /packages/react-codesandboxer/README.md: -------------------------------------------------------------------------------- 1 | # React-CodeSandboxer 2 | 3 | A simple react component that allows you to deploy example code to 4 | `Codesandbox`. It can take a `file` content, or fetch an example file from 5 | github or bitbucket. 6 | 7 | For fetching files, it will add both internal and external imports to the 8 | example, allowing you to build complex examples when you need to. 9 | 10 | This is a client-side implementation of a workflow using `codesandboxer` 11 | 12 | ```js 13 | import React, { Component } from 'react'; 14 | import CodeSandboxer from 'react-codesandboxer'; 15 | 16 | export default () => ( 17 | 25 | {() => } 26 | 27 | ); 28 | ``` 29 | 30 | ## What does this actually do? 31 | 32 | With the minimal options provided, the sandboxer can fetch the file contents 33 | from github, as well as the relevant imports of that file (both internal and 34 | external) 35 | 36 | ## Quick gotchas 37 | 38 | 1. If the example file does not exist at the source, the deploy will fail. You 39 | can get around this by passing in the file's contents directly as the prop 40 | `example`. 41 | 2. We follow relative imports in the example, so the example still works when 42 | uploaded. The fewer files your example depends upon, the faster it will be. 43 | (we will only ever fetch a file once, even if multiple things depend upon it) 44 | 3. While it's not enforced, making sure you have a button with the type 'sumbit' 45 | at the top level is important for accessibility. 46 | 4. You may find console errors from failed fetch requests. Codesandboxer 47 | captures and handles these errors, but we cannot stop them appearing in the 48 | console. See [minutiae](../../MINUTIAE.md) for details on why. 49 | 50 | ## Props we super definitely need 51 | 52 | With `gitInfo` and `examplePath`, we can take care of everything else for you, 53 | fetching all the example, and then fetching all other imports. Without this, 54 | things break. 55 | 56 | ### `examplePath: string` 57 | 58 | The absolute path to the example within the git file structure. This is used for 59 | fetching the example and other files that exist relative to the example. 60 | 61 | ### `gitInfo: GitInfo` 62 | 63 | This is all the information we need to fetch information from github or 64 | bitbucket. The format is: 65 | 66 | ``` 67 | { 68 | account: string, 69 | repository: string, 70 | branch?: string, 71 | host: 'bitbucket' | 'github', 72 | } 73 | ``` 74 | 75 | If no branch is provided, you will have your code deployed from master. Host is 76 | not defaulted. 77 | 78 | ## Props You Definitely Probably Want To Provide 79 | 80 | While these props aren't necessary to have codesandboxer work, you will almost 81 | always want to configure these, to make sure the example you get on CodeSandbox 82 | is the same as the example when run in its local context. 83 | 84 | ### `children: ({ error, isLoading, isDeploying, sandboxId, sandboxUrl }) => Node` 85 | 86 | Render prop that return `isLoading`, `files` and `error`. This is the 87 | recommended way to respond to the contents of `react-codesandboxer` if you want 88 | to change the appearance of the button. 89 | 90 | Children also receives the `sandboxId` and the `sandboxUrl` once those exist. 91 | 92 | ### `pkgJSON?: Package | string | Promise` 93 | 94 | The contents of the `package.json`. This is used to find the correct versions 95 | for imported npm packages used in your example and other files pulled in. If no 96 | package.json is provided, each package will use `latest` from npm. It has 97 | effectively 4 ways to pass in the package.JSON 98 | 99 | - Pass in the package itself as an object. 100 | - Pass in a string which is the git path to the package 101 | - Pass in a promise that resolves to: 102 | - An object that represents the package.json 103 | - A stringified version of the package.json object. 104 | 105 | ### `importReplacements?: Array<[string, string]>` 106 | 107 | Paths in the example that we do not want to be pulled from their relative 108 | location. These should be given as absolute git paths. This is most commonly 109 | used if your examples are pulling in something such as `src`, and you want to 110 | rely on the npm version of the component in codesandbox. 111 | 112 | ### `dependencies?: { [string]: string }` 113 | 114 | Dependencies to always include, even if they are not found in any file that was 115 | passed in. If you are using `importReplacements`, anything that is being added 116 | by it should go here as well. We always include react and react-dom for you. 117 | 118 | ## Cool Props To Add Niceness 119 | 120 | These props are less needed, and more to allow different use-cases, or some 121 | amount of debugging. You do not need to worry too much about them, but you can 122 | get some cool things done using them. 123 | 124 | ### `example?: string | Promise` 125 | 126 | Pass in the example as code to prevent it being fetched. This can be used when 127 | you want to perform any transformation on the example. If you pass in a promise, 128 | the returned value of the promise will be used. This can be useful if you are 129 | performing your own fetch or similar to get your example's raw contents. 130 | 131 | ### `name: string` 132 | 133 | Name for the CodeSandbox instance. This sets the package name in the uploaded 134 | `package.json`, which in turn sets the sandbox name. 135 | 136 | ### `afterDeployError?: ({ name: string, description?: string, content?: string, }) => mixed` 137 | 138 | Function that is called when an error occurs in the deploy process, with details 139 | of the error. 140 | 141 | ### `autoDeploy?: boolean` 142 | 143 | Deploy the sandbox when component mounts, instead of waiting for the button to 144 | be clicked. You should only need to autodeploy if you plan on opening the 145 | sandbox immediately, such as in an iframe. 146 | 147 | ### `preload?: boolean` 148 | 149 | Load the files when component mounts, instead of waiting for the button to be 150 | clicked. This doesn't deploy the sandbox on mount yet, it only preloads all the 151 | data it needs to deploy. 152 | 153 | ### onLoadComplete?: ({ parameters: string, files: Files } | { error: any }) => mixed, 154 | 155 | Function called once loading has finished, whether this is from preload or from 156 | a button press. It returns an object with the parameters string to submit to 157 | CodeSandbox as well as the unprocessed files object. If there is an error, the 158 | error will be returned instead. 159 | 160 | ### `afterDeploy?: (sandboxUrl: string, sandboxId: string) => mixed` 161 | 162 | Function called once the deploy has occurred. This function is given both the 163 | base sandboxUrl that we open by default, as well as the sandboxId so you can 164 | generate your own urls, such as an embed url. 165 | 166 | ### `providedFiles?: Files` 167 | 168 | Pass in files separately to fetching them. Useful to go alongside specific 169 | replacements in importReplacements. 170 | 171 | The shape of the files object is 172 | 173 | ``` 174 | { 175 | fileName: { 176 | content: string 177 | } 178 | } 179 | ``` 180 | 181 | The filename is the absolute path where it will be created on CodeSandbox, and 182 | the content is the file's contents as a string. 183 | 184 | If a fileName exists in your provided files, it will not be fetched when it is 185 | referenced. 186 | 187 | ### `skipRedirect?: boolean` 188 | 189 | Do not open the sandbox once the data has been sent. Using this along with the 190 | `afterDeploy` prop can allow you to handle what is done with the sandbox, 191 | including loading an embed using the ID. 192 | 193 | #### `extensions?: Array` 194 | 195 | An array of extensions that will be treated as javascript files. For example, if 196 | you pass in [`.jsx`], when loading files, we will attempt to fetch `.jsx` files 197 | as well as `.js` and `.json` files. The extension type of your example is 198 | automatically added, so if you pass in the `examplePath` `my/cool/example.jsx`, 199 | you will not need to pass in the jsx extension. 200 | 201 | If your example file is fo type `.ts` or `.tsx` both are added. 202 | 203 | ### `template?: string` 204 | 205 | This template prop sets what CodeSandbox template to use. Currently we support: 206 | 207 | - `create-react-app` 208 | - `create-react-app-typescript` 209 | 210 | We auto-detect which one we think we should use, so you should only need to 211 | provide this if you want to override our selected template. 212 | 213 | Unsupported templates will still cause the bundled files to be sent to 214 | CodeSandbox under that template, but the bundling may fail. 215 | 216 | ## A slightly more complicated example: 217 | 218 | ```js 219 | import pkgJSON from '../package.json'; 220 | 221 | 238 | {({ isLoading, error }) => 239 | isLoading 240 | ?
Uploading
241 | : ( 242 | 245 | ) 246 | } 247 |
248 | ``` 249 | 250 | This shows off some more advanced usage: 251 | 252 | - We are providing the pkgJSON as an object, so it does not need to be fetched 253 | (this also accepts a promise that resolves to a pkgJSON) 254 | - We are specifying a branch to pull files from (also accepts git hashes) 255 | - We are replacing src files with a reference to the package, so the package's 256 | own internals are pulled from npm (this is a nice optimisation in component 257 | docs) 258 | - We have a collection of dependencies we always include 259 | - We are providing our own index, allowing us to add the `css-reset` to it. We 260 | can also pass in extra files 261 | - We are logging after a deploy 262 | - We have provided a custom button component 263 | -------------------------------------------------------------------------------- /packages/react-codesandboxer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-codesandboxer", 3 | "version": "3.1.5", 4 | "description": "A simple React component to help easily deploy an example to CodeSandbox", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/codesandbox/codesandboxer", 8 | "directory": "packages/react-codesandboxer" 9 | }, 10 | "license": "MIT", 11 | "author": "Ben Conolly", 12 | "files": [ 13 | "dist" 14 | ], 15 | "main": "dist/index.js", 16 | "scripts": { 17 | "build": "babel src -d dist --ignore **/*.test.js" 18 | }, 19 | "dependencies": { 20 | "codesandboxer": "^1.0.3", 21 | "lodash.isequal": "^4.5.0", 22 | "lodash.pick": "^4.4.0", 23 | "react-node-resolver": "^1.0.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/react-codesandboxer/src/CodeSandboxer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component, type Node } from 'react'; 3 | import NodeResolver from 'react-node-resolver'; 4 | import isEqual from 'lodash.isequal'; 5 | import pick from 'lodash.pick'; 6 | 7 | import { 8 | fetchFiles, 9 | sendFilesToCSB, 10 | getSandboxUrl, 11 | finaliseCSB, 12 | type Package, 13 | type Files, 14 | type GitInfo, 15 | type ImportReplacement, 16 | } from 'codesandboxer'; 17 | 18 | type Error = { 19 | name: string, 20 | description?: string, 21 | content?: string, 22 | }; 23 | 24 | type State = { 25 | parameters: string, 26 | isLoading: boolean, 27 | isDeploying: boolean, 28 | sandboxId?: string, 29 | sandboxUrl?: string, 30 | deployPromise?: Promise, 31 | files?: Files, 32 | error?: Error, 33 | fileName: string, 34 | }; 35 | 36 | type Props = { 37 | /* The absolute path to the example within the git file structure */ 38 | examplePath: string, 39 | /* Name for the CodeSandbox instance */ 40 | name?: string, 41 | /* This is all the information we need to fetch information from github or bitbucket */ 42 | gitInfo: GitInfo, 43 | /* Pass in the example as code to prevent it being fetched */ 44 | example?: string | Promise, 45 | /* Either take in a package.json object, or a string as the path of the package.json */ 46 | pkgJSON?: Package | string | Promise, 47 | /* paths in the example that we do not want to be pulled from their relativeLocation */ 48 | importReplacements?: Array, 49 | /* Dependencies we always include. Most likely react and react-dom */ 50 | dependencies?: { [string]: string }, 51 | /* Do not actually deploy to codesanbox. Used to for testing alongside the return values of the render prop. */ 52 | skipRedirect?: boolean, 53 | ignoreInternalImports?: boolean, 54 | /* Load the files when component mounts, instead of waiting for the button to be clicked */ 55 | preload?: boolean, 56 | /* Deploy the sandbox when component mounts, instead of waiting for the button to be clicked */ 57 | autoDeploy?: boolean, 58 | /* Called once loading has finished, whether it preloaded or not */ 59 | onLoadComplete?: ( 60 | { parameters: string, files: Files } | { error: any } 61 | ) => mixed, 62 | /* Called once a deploy has occurred. This will still be called if skipRedirect is chosen */ 63 | afterDeploy?: (sandboxUrl: string, sandboxId: string) => mixed, 64 | /* Called once a deploy has occurred. This will still be called if skipRedirect is chosen */ 65 | afterDeployError?: Error => mixed, 66 | /* Pass in files separately to fetching them. Useful to go alongisde specific replacements in importReplacements */ 67 | providedFiles?: Files, 68 | /* Render prop that return `isLoading`and `error`. */ 69 | children: (obj: { 70 | isLoading: boolean, 71 | files?: Files, 72 | sandboxId?: string, 73 | sandboxUrl?: string, 74 | }) => Node, 75 | /* Consumers may need access to the wrapper's style */ 76 | style: Object, 77 | /* allow codesandboxer to accept jsx properties */ 78 | extensions: string[], 79 | template?: 'create-react-app' | 'create-react-app-typescript' | 'vue-cli', 80 | }; 81 | 82 | export default class CodeSandboxDeployer extends Component { 83 | button: HTMLElement | null; 84 | 85 | state: State = { 86 | parameters: '', 87 | isLoading: false, 88 | isDeploying: false, 89 | fileName: 'example', 90 | }; 91 | static defaultProps = { 92 | children: () => , 93 | pkgJSON: {}, 94 | dependencies: {}, 95 | providedFiles: {}, 96 | importReplacements: [], 97 | extensions: [], 98 | style: { display: 'inline-block' }, 99 | }; 100 | 101 | shouldReload = false; 102 | 103 | loadFiles = () => { 104 | let { onLoadComplete, providedFiles, dependencies, name } = this.props; 105 | 106 | // by assembling a deploy promise, we can save it for later if loadFiles is 107 | // being called by `preload`, and preload can use it once it is ready. 108 | // We return deployPromise at the end so that non-preloaded calls can then be 109 | // resolved 110 | let deployPromise = fetchFiles(this.props) 111 | .then(fetchedInfo => { 112 | let { parameters } = finaliseCSB(fetchedInfo, { 113 | extraFiles: providedFiles, 114 | extraDependencies: dependencies, 115 | name, 116 | }); 117 | this.setState( 118 | { 119 | parameters, 120 | isLoading: false, 121 | files: fetchedInfo.files, 122 | fileName: fetchedInfo.fileName, 123 | }, 124 | () => { 125 | if (onLoadComplete) { 126 | onLoadComplete({ parameters, files: fetchedInfo.files }); 127 | } 128 | } 129 | ); 130 | }) 131 | .catch(error => { 132 | this.setState({ error, isLoading: false }); 133 | if (onLoadComplete) onLoadComplete({ error }); 134 | }); 135 | 136 | this.setState({ 137 | isLoading: true, 138 | deployPromise, 139 | }); 140 | 141 | return deployPromise; 142 | }; 143 | 144 | deploy = () => { 145 | let { afterDeploy, skipRedirect, afterDeployError } = this.props; 146 | let { parameters, error, fileName } = this.state; 147 | if (error) return; 148 | 149 | sendFilesToCSB(parameters, { fileName }) 150 | .then(({ sandboxId, sandboxUrl }) => { 151 | this.setState({ 152 | sandboxId, 153 | sandboxUrl, 154 | isDeploying: false, 155 | isLoading: false, 156 | }); 157 | if (!skipRedirect) { 158 | window.open(sandboxUrl); 159 | } 160 | if (afterDeploy) { 161 | afterDeploy(getSandboxUrl(sandboxId, 'embed'), sandboxId); 162 | } 163 | }) 164 | .catch(errors => { 165 | if (afterDeployError) { 166 | afterDeployError({ 167 | name: 'error deploying to CodeSandbox', 168 | content: errors, 169 | }); 170 | } 171 | this.setState({ 172 | error: { 173 | name: 'error deploying to CodeSandbox', 174 | content: errors, 175 | }, 176 | }); 177 | }); 178 | }; 179 | 180 | deployToCSB = (e?: MouseEvent) => { 181 | const { deployPromise, isDeploying } = this.state; 182 | if (e) { 183 | e.preventDefault(); 184 | } 185 | if (isDeploying) return null; 186 | this.setState({ isDeploying: true }); 187 | 188 | if (!this.shouldReload && deployPromise) { 189 | deployPromise.then(this.deploy); 190 | } else { 191 | this.shouldReload = false; 192 | this.loadFiles().then(this.deploy); 193 | } 194 | }; 195 | 196 | componentDidUpdate(prevProps: Props) { 197 | /* If props related to loading files have been changed, next deploy should reload files */ 198 | /* The props that are compared should be the same as the arguments of fetchFiles */ 199 | const compareKeys = [ 200 | 'examplePath', 201 | 'gitInfo', 202 | 'importReplacements', 203 | 'dependencies', 204 | 'providedFiles', 205 | 'name', 206 | 'extensions', 207 | 'template', 208 | ]; 209 | if (!isEqual(pick(this.props, compareKeys), pick(prevProps, compareKeys))) { 210 | this.shouldReload = true; 211 | } else { 212 | /* pkgJSON and example also need to be compared, but may be promises, which must be resolved before they can be compared */ 213 | Promise.all([this.props.example, prevProps.example]).then( 214 | ([example, prevExample]) => { 215 | if (example !== prevExample) { 216 | this.shouldReload = true; 217 | } else { 218 | Promise.all([this.props.pkgJSON, prevProps.pkgJSON]).then( 219 | ([pkgJSON, prevPkgJSON]) => { 220 | if (!isEqual(pkgJSON, prevPkgJSON)) { 221 | this.shouldReload = true; 222 | } 223 | } 224 | ); 225 | } 226 | } 227 | ); 228 | } 229 | } 230 | 231 | componentDidMount() { 232 | if (this.props.autoDeploy) { 233 | this.deployToCSB(); 234 | return; 235 | } 236 | 237 | if (this.button) this.button.addEventListener('click', this.deployToCSB); 238 | if (this.props.preload) this.loadFiles(); 239 | } 240 | componentWillUnmount() { 241 | if (this.button) this.button.removeEventListener('click', this.deployToCSB); 242 | } 243 | 244 | getButton = (ref: HTMLElement | null) => { 245 | if (!ref) return; 246 | this.button = ref; 247 | }; 248 | 249 | render() { 250 | const { isLoading, isDeploying, error, sandboxId, sandboxUrl } = this.state; 251 | return ( 252 | 253 | {this.props.children({ 254 | isLoading, 255 | isDeploying, 256 | error, 257 | sandboxId, 258 | sandboxUrl, 259 | })} 260 | 261 | ); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /packages/react-codesandboxer/src/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './CodeSandboxer'; 2 | -------------------------------------------------------------------------------- /packages/vs-codesandboxer/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "no-const-assign": "warn", 16 | "no-this-before-super": "warn", 17 | "no-undef": "warn", 18 | "no-unreachable": "warn", 19 | "no-unused-vars": "warn", 20 | "constructor-super": "warn", 21 | "valid-typeof": "warn" 22 | } 23 | } -------------------------------------------------------------------------------- /packages/vs-codesandboxer/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode-test/ 3 | *.vsix 4 | -------------------------------------------------------------------------------- /packages/vs-codesandboxer/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /packages/vs-codesandboxer/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: e 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ] 16 | }, 17 | { 18 | "name": "Extension Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}", 24 | "--extensionTestsPath=${workspaceFolder}/test" 25 | ] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /packages/vs-codesandboxer/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | } -------------------------------------------------------------------------------- /packages/vs-codesandboxer/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | jsconfig.json 6 | vsc-extension-quickstart.md 7 | .eslintrc.json 8 | -------------------------------------------------------------------------------- /packages/vs-codesandboxer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.4.3 4 | 5 | - Updated dependencies 6 | [b46e059](https://github.com/codesandbox/codesandboxer/commit/b46e059): 7 | - codesandboxer-fs@1.0.0 8 | 9 | ## 0.3.0 10 | 11 | Now supports TypeScript sandboxes 12 | 13 | ## 0.2.5 14 | 15 | Upgrade dependencies 16 | Thanks to improvements in codesandboxer-fs, files with alternate extensions 17 | (such as `.jsx`) can now be used as entry files and they will collect other 18 | files of the same type. 19 | 20 | ## 0.2.0 21 | 22 | Fix pernicious path resolution bug 23 | 24 | ## 0.0.1 25 | 26 | Initial Release. Very unstable, do not rely upon it. 27 | -------------------------------------------------------------------------------- /packages/vs-codesandboxer/README.md: -------------------------------------------------------------------------------- 1 | # vs-codesandboxer 2 | 3 | Open a component from your editor into codesandboxer, using `codesandboxer-fs` 4 | under-the-hood. 5 | 6 | Can be used with the command `Deploy to CodeSandbox` or right click in your 7 | active file to select to deploy it. Once your needed files/dependencies have 8 | been sorted out, an 'open in CodeSandbox' link will open as a notification. 9 | Click it and you'll be able to see your component in codesandbox, and share it 10 | with others. 11 | 12 | ## Main use-cases 13 | 14 | Sharing! When you want to get opinions on changes to a component but are not 15 | publishing built isolated version anywhere, you can open it here and share it 16 | more easily. 17 | 18 | This can help teams that are working asynchronously/remotely share proposed 19 | changes easily and start getting feedback, outside heavier more robust git-based 20 | processes. 21 | 22 | ## Provisos 23 | 24 | We use auto-detection for the kind of sandbox we should create. You should use 25 | `codesandboxer-fs` through the command line if you need to pass in more options. 26 | 27 | The only sandboxes we currently support are: 28 | 29 | - create-react-app 30 | - create-react-app-typescript 31 | - vue-cli 32 | -------------------------------------------------------------------------------- /packages/vs-codesandboxer/extension.js: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | const vscode = require('vscode'); 4 | const csb = require('codesandboxer-fs'); 5 | 6 | // this method is called when your extension is activated 7 | // your extension is activated the very first time the command is executed 8 | function activate(context) { 9 | // The command has been defined in the package.json file 10 | // Now provide the implementation of the command with registerCommand 11 | // The commandId parameter must match the command field in package.json 12 | let disposable = vscode.commands.registerCommand( 13 | 'extension.vs-codesandboxer', 14 | function() { 15 | // The code you place here will be executed every time your command is executed 16 | // Display a message box to the user 17 | let filePath = vscode.window.activeTextEditor.document.fileName; 18 | 19 | vscode.window.showInformationMessage('uploading to codesandbox...'); 20 | 21 | csb 22 | .assembleFilesAndPost(filePath) 23 | .then(sandboxInfo => { 24 | vscode.window.showInformationMessage( 25 | `[Open file in codesandbox](${sandboxInfo.sandboxUrl})` 26 | ); 27 | }) 28 | .catch(e => { 29 | vscode.window.showErrorMessage( 30 | `Error deploying to codesandbox: ${e}` 31 | ); 32 | console.log('an error was thrown', e); 33 | }); 34 | } 35 | ); 36 | 37 | context.subscriptions.push(disposable); 38 | } 39 | exports.activate = activate; 40 | -------------------------------------------------------------------------------- /packages/vs-codesandboxer/logger.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | 3 | const outputChannel = vscode.window.createOutputChannel('codesandboxer'); 4 | 5 | function log(msg) { 6 | console.log(msg); 7 | outputChannel.appendLine(msg); 8 | } 9 | 10 | module.exports = log; 11 | -------------------------------------------------------------------------------- /packages/vs-codesandboxer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vs-codesandboxer", 3 | "version": "1.0.0", 4 | "description": "Upload to CodeSandbox from a single entry file", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/codesandbox/codesandboxer", 8 | "directory": "packages/vs-codesandboxer" 9 | }, 10 | "main": "./extension", 11 | "scripts": { 12 | "build": "echo done", 13 | "test": "node ./node_modules/vscode/bin/test" 14 | }, 15 | "dependencies": { 16 | "codesandboxer-fs": "^1.0.0", 17 | "pkg-dir": "^2.0.0", 18 | "pkg-up": "^2.0.0" 19 | }, 20 | "devDependencies": { 21 | "@types/mocha": "^2.2.42", 22 | "@types/node": "^7.0.43", 23 | "eslint": "^4.19.1", 24 | "typescript": "^2.6.1", 25 | "vscode": "^1.1.6" 26 | }, 27 | "engines": { 28 | "vscode": "^1.23.0" 29 | }, 30 | "activationEvents": [ 31 | "onCommand:extension.vs-codesandboxer" 32 | ], 33 | "contributes": { 34 | "commands": [ 35 | { 36 | "command": "extension.vs-codesandboxer", 37 | "title": "Deploy to CodeSandbox" 38 | } 39 | ], 40 | "menus": { 41 | "editor/context": [ 42 | { 43 | "when": "resourceLangId == javascript", 44 | "command": "extension.vs-codesandboxer" 45 | }, 46 | { 47 | "when": "resourceLangId == vue", 48 | "command": "extension.vs-codesandboxer" 49 | }, 50 | { 51 | "when": "resourceLangId == typescriptreact", 52 | "command": "extension.vs-codesandboxer" 53 | }, 54 | { 55 | "when": "resourceLangId == javascriptreact", 56 | "command": "extension.vs-codesandboxer" 57 | } 58 | ] 59 | } 60 | }, 61 | "displayName": "vs-codesandboxer", 62 | "publisher": "noviny" 63 | } 64 | -------------------------------------------------------------------------------- /packages/vs-codesandboxer/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | - This folder contains all of the files necessary for your extension. 6 | - `package.json` - this is the manifest file in which you declare your extension 7 | and command. The sample plugin registers a command and defines its title and 8 | command name. With this information VS Code can show the command in the 9 | command palette. It doesn’t yet need to load the plugin. 10 | - `extension.js` - this is the main file where you will provide the 11 | implementation of your command. The file exports one function, `activate`, 12 | which is called the very first time your extension is activated (in this case 13 | by executing the command). Inside the `activate` function we call 14 | `registerCommand`. We pass the function containing the implementation of the 15 | command as the second parameter to `registerCommand`. 16 | 17 | ## Get up and running straight away 18 | 19 | - Press `F5` to open a new window with your extension loaded. 20 | - Run your command from the command palette by pressing (`Ctrl+Shift+P` or 21 | `Cmd+Shift+P` on Mac) and typing `Hello World`. 22 | - Set breakpoints in your code inside `extension.js` to debug your extension. 23 | - Find output from your extension in the debug console. 24 | 25 | ## Make changes 26 | 27 | - You can relaunch the extension from the debug toolbar after changing code in 28 | `extension.js`. 29 | - You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your 30 | extension to load your changes. 31 | 32 | ## Explore the API 33 | 34 | - You can open the full set of our API when you open the file 35 | `node_modules/vscode/vscode.d.ts`. 36 | 37 | ## Run tests 38 | 39 | - Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the 40 | launch configuration dropdown pick `Launch Tests`. 41 | - Press `F5` to run the tests in a new window with your extension loaded. 42 | - See the output of the test result in the debug console. 43 | - Make changes to `test/extension.test.js` or create new test files inside the 44 | `test` folder. 45 | - By convention, the test runner will only consider files matching the name 46 | pattern `**.test.js`. 47 | - You can create folders inside the `test` folder to structure your tests any 48 | way you want. 49 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | --------------------------------------------------------------------------------