├── .github
└── ISSUE_TEMPLATE
│ └── feedback.md
├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── .travis.yml
├── LICENSE
├── README.md
├── assets
├── nunjucks-demo.png
└── scenario-previews.png
├── demos
├── nunjucks
│ ├── .gitignore
│ ├── README.md
│ ├── fractal.config.js
│ ├── package-lock.json
│ ├── package.json
│ ├── pages
│ │ └── about.md
│ └── src
│ │ ├── assets
│ │ ├── main.js
│ │ ├── main.scss
│ │ └── preview.js
│ │ └── components
│ │ ├── 01-units
│ │ ├── button
│ │ │ ├── arrow-left.svg
│ │ │ ├── arrow-right.svg
│ │ │ ├── button.config.js
│ │ │ ├── button.js
│ │ │ ├── button.scss
│ │ │ └── view.njk
│ │ └── figure
│ │ │ ├── assets
│ │ │ └── placeholder.jpg
│ │ │ ├── figure.config.js
│ │ │ ├── figure.scss
│ │ │ └── view.njk
│ │ └── 02-patterns
│ │ ├── card-set
│ │ ├── card-set.config.js
│ │ ├── card-set.scss
│ │ └── view.njk
│ │ └── card
│ │ ├── card.config.js
│ │ ├── card.scss
│ │ ├── notes.md
│ │ └── view.njk
└── vue
│ ├── .gitignore
│ ├── README.md
│ ├── fractal.config.js
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ └── components
│ ├── counter
│ ├── counter.config.js
│ └── counter.vue
│ └── multi-counter
│ ├── multi-counter.config.js
│ └── multi-counter.vue
├── jest.config.js
├── lerna.json
├── package-lock.json
├── package.json
├── packages
├── adapter-nunjucks
│ ├── README.md
│ ├── index.js
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ │ ├── adapter.js
│ │ ├── env.js
│ │ ├── extension-component.js
│ │ └── loader.js
├── adapter-vue
│ ├── index.js
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ │ └── adapter.js
├── app
│ ├── README.md
│ ├── index.js
│ ├── package-lock.json
│ ├── package.json
│ └── src
│ │ ├── app.js
│ │ ├── builder.js
│ │ ├── include-raw.js
│ │ ├── mode.js
│ │ ├── resources.js
│ │ ├── router.js
│ │ ├── serve.js
│ │ ├── views-loader.js
│ │ └── views.js
├── core
│ ├── README.md
│ ├── helpers.js
│ ├── index.js
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── compile-components.js
│ │ ├── compile-components.test.js
│ │ ├── compiler.js
│ │ ├── compiler.test.js
│ │ ├── component-matcher.js
│ │ ├── compose.js
│ │ ├── compose.test.js
│ │ ├── create-adapter.js
│ │ ├── entities
│ │ │ ├── component.js
│ │ │ ├── entity.js
│ │ │ └── file.js
│ │ ├── helpers.js
│ │ ├── html-adapter.js
│ │ ├── middleware
│ │ │ ├── config.js
│ │ │ ├── files.js
│ │ │ ├── index.js
│ │ │ ├── label.js
│ │ │ ├── name.js
│ │ │ └── scenarios.js
│ │ ├── read.js
│ │ ├── read.test.js
│ │ ├── state.js
│ │ └── state.test.js
│ └── test
│ │ ├── .gitignore
│ │ ├── fixtures
│ │ ├── components
│ │ │ ├── @standard
│ │ │ │ ├── standard.config.js
│ │ │ │ └── view.html
│ │ │ └── nested
│ │ │ │ └── @yaml
│ │ │ │ ├── view.html
│ │ │ │ └── yaml.config.yml
│ │ ├── custom-matcher
│ │ │ └── component-standard
│ │ │ │ └── view.html
│ │ └── ignored
│ │ │ └── @with-node-modules
│ │ │ ├── node_modules
│ │ │ └── ignored.js
│ │ │ └── view.html
│ │ └── integration.test.js
├── fractalite
│ ├── .babelrc
│ ├── .gitignore
│ ├── README.md
│ ├── assets
│ │ ├── app.js
│ │ ├── app.scss
│ │ ├── css
│ │ │ ├── app.scss
│ │ │ ├── base.scss
│ │ │ ├── brand.scss
│ │ │ ├── controls.scss
│ │ │ ├── error.scss
│ │ │ ├── highlight.scss
│ │ │ ├── inspector.scss
│ │ │ ├── json-explorer.scss
│ │ │ ├── nav.scss
│ │ │ ├── page.scss
│ │ │ ├── panels.scss
│ │ │ ├── preview.scss
│ │ │ ├── proptable.scss
│ │ │ ├── prose.scss
│ │ │ ├── search.scss
│ │ │ ├── source-code.scss
│ │ │ ├── split.scss
│ │ │ ├── tabs.scss
│ │ │ └── theme.scss
│ │ └── reload.js
│ ├── bin
│ │ └── fractalite
│ ├── index.js
│ ├── package-lock.json
│ ├── package.json
│ ├── pages
│ │ └── index.njk
│ ├── src
│ │ ├── client
│ │ │ ├── app.js
│ │ │ ├── components
│ │ │ │ ├── app-link.js
│ │ │ │ ├── error.js
│ │ │ │ ├── inspector.js
│ │ │ │ ├── json-explorer.js
│ │ │ │ ├── navigation.js
│ │ │ │ ├── page.js
│ │ │ │ ├── preview.js
│ │ │ │ ├── search.js
│ │ │ │ ├── source-code.js
│ │ │ │ └── split.js
│ │ │ ├── events.js
│ │ │ ├── router.js
│ │ │ └── store.js
│ │ └── server
│ │ │ ├── inspector.js
│ │ │ ├── nav.js
│ │ │ ├── pages.js
│ │ │ ├── plugins
│ │ │ ├── index.js
│ │ │ ├── inspector-html.js
│ │ │ ├── inspector-info.js
│ │ │ └── inspector-props.js
│ │ │ ├── preview.js
│ │ │ ├── public.js
│ │ │ ├── references.js
│ │ │ ├── search.js
│ │ │ ├── theme.js
│ │ │ └── utils
│ │ │ ├── highlight.js
│ │ │ ├── markdown.js
│ │ │ ├── prettify.js
│ │ │ └── resolve-asset.js
│ └── views
│ │ ├── app.njk
│ │ ├── error.njk
│ │ ├── layout.njk
│ │ ├── partials
│ │ └── brand.njk
│ │ ├── plugins
│ │ └── inspector-info.njk
│ │ ├── preview.njk
│ │ └── vue
│ │ ├── error.html
│ │ ├── inspector.html
│ │ ├── navigation.html
│ │ ├── page.html
│ │ ├── preview.html
│ │ └── search.html
├── plugin-assets-bundler
│ ├── README.md
│ ├── index.js
│ ├── package-lock.json
│ └── package.json
├── plugin-notes
│ ├── README.md
│ ├── index.js
│ ├── package-lock.json
│ └── package.json
└── support
│ ├── README.md
│ ├── html.js
│ ├── index.js
│ ├── load-config.js
│ ├── package-lock.json
│ ├── package.json
│ └── utils.js
└── test
└── helpers
└── generate-components.js
/.github/ISSUE_TEMPLATE/feedback.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feedback
3 | about: 'Provide feedback on the Fractalite prototype '
4 | title: 'Feedback '
5 | labels: Feedback
6 | assignees: allmarkedup
7 | ---
8 |
9 | (Thanks for taking the time to submit feedback! We've supplied some questions
10 | below to try and help standardise responses, but feel free to skip/delete/amend as you see fit.)
11 |
12 | ## Initial thoughts
13 |
14 | #### Did you have any difficulties getting the prototype demo up and running?
15 |
16 | #### Were your initial gut feelings about the prototype direction positive or negative?
17 |
18 | #### Do you think this prototype represents a good development direction for the next version of Fractal?
19 |
20 | ## Components
21 |
22 | #### Did you like the `@name` folder name convention for components?
23 |
24 | #### v1-style 'single file components' are no longer supported. Does that pose any difficulties for you?
25 |
26 | #### Do you have any thoughts on the naming of configuration properties?
27 |
28 | (For example, v1 'variants' and 'context data' config properties have been renamed to 'scenarios' and 'props' respectively. Are those names better/worse/the same?)
29 |
30 | ## Adapters
31 |
32 | #### Which template engines/frameworks would you most like to see integration with? (2 - 3 max)
33 |
34 | - [ ] DustJS
35 | - [ ] HAML
36 | - [ ] Handlebars
37 | - [ ] Marko
38 | - [ ] Mustache
39 | - [ ] Nunjucks
40 | - [ ] Pug
41 | - [ ] React
42 | - [ ] Twig
43 | - [ ] Vue
44 | - [ ] Other - please specify below
45 |
46 | ## For developers...
47 |
48 | #### Does the plugin model make sense to you? Any questions/queries about it?
49 |
50 | #### Are there customisations that you'd like to make that you think might not be possible in the current plugin system implementation?
51 |
52 | #### Does the Adapter system implementation make sense you you?
53 |
54 | #### Do you think the monorepo approach is a good route future Fractal development?
55 |
56 | ## Other
57 |
58 | #### Is there anything missing from the prototype that currently exists in v1 and that you think should be part of the core of any future versions (i.e. not implemented as a plugin)?
59 |
60 | #### Any other comments/suggestions/ideas?
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | *.log
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 | lib-cov
7 | coverage
8 | .nyc_output
9 | node_modules/
10 | .npm
11 | .eslintcache
12 | .node_repl_history
13 | *.tgz
14 | .yarn-integrity
15 | .env
16 | .wip*
17 | /dist
18 | .cache
19 | .wip-packages
20 | .demo-*
21 | test/fixtures/generated
22 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .*rc
2 | README.md
3 | docs
4 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 200,
3 | singleQuote: true,
4 | bracketSpacing: true,
5 | useTabs: false
6 | };
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | os:
2 | - windows
3 | - linux
4 | - osx
5 | language: node_js
6 | node_js:
7 | - lts/dubnium
8 | - '11.10.1'
9 | sudo: false
10 | before_script:
11 | - npm run bootstrap
12 | script:
13 | - 'npm run test:ci'
14 | cache:
15 | directories:
16 | - node_modules
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Mark Perkins
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/assets/nunjucks-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frctl/fractalite/8e1dd708265df58921e9061d787e59dd3022d1d6/assets/nunjucks-demo.png
--------------------------------------------------------------------------------
/assets/scenario-previews.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frctl/fractalite/8e1dd708265df58921e9061d787e59dd3022d1d6/assets/scenario-previews.png
--------------------------------------------------------------------------------
/demos/nunjucks/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | build
3 |
--------------------------------------------------------------------------------
/demos/nunjucks/README.md:
--------------------------------------------------------------------------------
1 | ## Nunjucks demo project
2 |
3 | A Fractalite demo using the Nunjucks adapter and the asset bundler plugin.
4 |
--------------------------------------------------------------------------------
/demos/nunjucks/fractal.config.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 |
3 | module.exports = {
4 | /*
5 | * Project title used in the UI
6 | */
7 | title: 'Nunjucks Demo',
8 |
9 | /*
10 | * Template engine adapter
11 | *
12 | * Adapters are responsible for implementing component
13 | * rendering but can also hook into the component compilation
14 | * and UI preview rendering steps if required.
15 | */
16 | adapter: require('@frctl/fractalite-adapter-nunjucks')({
17 | // Adapter options here...
18 | }),
19 |
20 | /*
21 | * Basic theme tweaks can be done via config here.
22 | * The `vars` object is a set of CSS variable overrides
23 | * that will be applied to the UI.
24 | */
25 | theme: {
26 | vars: {
27 | 'link-color': '#0074d9'
28 | }
29 | },
30 |
31 | /*
32 | * Absolute path to the components directory.
33 | *
34 | * This can also be an array of directory paths and
35 | * can use glob syntax with negation to exclude certain
36 | * files from the compiler.
37 | *
38 | * `node_modules` directories within the components directory
39 | * are always ignored.
40 | */
41 | components: resolve(__dirname, './src/components'),
42 |
43 | /*
44 | * Absolute path to the pages directory, if required.
45 | *
46 | * Pages are markdown documents that can additionally be
47 | * run through a Nunjucks renderer to give dynamic access
48 | * to component information.
49 | */
50 | pages: resolve(__dirname, './pages'),
51 |
52 | nav: {
53 | /*
54 | * Optional navigation generator.
55 | *
56 | * If not provided the default navigation structure will be used.
57 | *
58 | * The value of the items property can either be an array of navigation
59 | * items or a function that returns an array of items. The function will
60 | * be called with a set of entities (such as components and pages) as well
61 | * as a toTree utility function for converting flat arrays of entities to
62 | * nav tree structures.
63 | *
64 | * Each item in the tree can have the following properties:
65 | *
66 | * - `label`: The text displayed for the nav item
67 | * - `url`: the URL to link to (if not a directory)
68 | * - `children`: An array of child navigation items
69 | */
70 | items({ components, pages }, toTree) {
71 | return [
72 | toTree(pages),
73 | {
74 | label: 'Components',
75 | children: toTree(components),
76 | expanded: true // open by default
77 | },
78 | {
79 | label: 'External links',
80 | expanded: true,
81 | children: [
82 | {
83 | label: 'Github repo →',
84 | url: 'http://github.com/frctl/fractalite'
85 | }
86 | ]
87 | }
88 | ];
89 | }
90 | },
91 |
92 | /*
93 | * Project plugins registry
94 | *
95 | * Plugins have full access to both the core component compiler
96 | * and the UI generator.
97 | *
98 | * A plugin is just a function that receives the main application
99 | * object. Most plugins will additionally wrap this function in another
100 | * that can accept options for user customisation.
101 | */
102 | plugins: [
103 | /*
104 | * The asset bundler plugin uses Parcel for zero-config
105 | * asset bundling and hot module reloading.
106 | * It also automatically serves bundled assets and
107 | * adds them to component previews.
108 | *
109 | * Whilst sufficient for most projects, those with more complex
110 | * asset build requirements may choose to use their own asset
111 | * bundler/development workflow.
112 | */
113 | require('@frctl/fractalite-plugin-assets-bundler')({
114 | entryFile: resolve(__dirname, './src/assets/preview.js'),
115 | outFile: resolve(__dirname, './dist/assets/build.js')
116 | }),
117 |
118 | /*
119 | * The notes plugin allows component notes to be
120 | * specified in component config files and/or (optionally)
121 | * read from a markdown file in the component directory.
122 | */
123 | require('@frctl/fractalite-plugin-notes')({
124 | notesFile: 'notes.md'
125 | })
126 | ]
127 | };
128 |
--------------------------------------------------------------------------------
/demos/nunjucks/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite-demo-nunjucks",
3 | "private": true,
4 | "scripts": {
5 | "start": "fractalite start --port 3030",
6 | "build": "fractalite build --dest ./build --serve --port 4040",
7 | "serve-static": "fractalite serve --dest ./build --port 4040"
8 | },
9 | "devDependencies": {
10 | "@frctl/fractalite": "^0.0.0",
11 | "@frctl/fractalite-adapter-nunjucks": "^0.0.0",
12 | "@frctl/fractalite-plugin-assets-bundler": "^0.0.0",
13 | "@frctl/fractalite-plugin-notes": "^0.0.0",
14 | "sass": "^1.17.1"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/demos/nunjucks/pages/about.md:
--------------------------------------------------------------------------------
1 | ---
2 | label: Using pages
3 | ---
4 |
5 | You can add as many pages to your styleguide as you like.
6 |
7 | Pages are written in markdown and code samples are highlighted automatically:
8 |
9 | ```html
10 |
Some HTML code here
11 | ```
12 |
13 | You can use reference tags to link to your components: [Check out the button →]({inspect:button})
14 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/assets/main.js:
--------------------------------------------------------------------------------
1 | import '../components/01-units/button/button.js';
2 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/assets/main.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | font-family: sans-serif;
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
8 | body {
9 | padding: 20px;
10 | }
11 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/assets/preview.js:
--------------------------------------------------------------------------------
1 | import './main.js';
2 |
3 | import './main.scss';
4 | import '../components/**/*.scss';
5 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/01-units/button/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/01-units/button/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/01-units/button/button.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | /*
3 | * Component `scenarios` are very similar to `variants` in
4 | * Fractal v1. They define a set of named component instances
5 | * that are then displayed in the UI.
6 | */
7 | scenarios: [
8 | {
9 | name: 'next',
10 | /*
11 | * The `props` object is similar to `context` in Fractal v1
12 | * Properties defined here are passed to the template engine adapter
13 | * when the component is being rendered.
14 | */
15 | props: {
16 | text: 'Next',
17 | modifier: 'action',
18 | icon: './arrow-right.svg', // Component-relative file paths are automatically converted to URLs
19 | iconPos: 'after'
20 | }
21 | },
22 | {
23 | name: 'prev',
24 | props: {
25 | text: 'Prev',
26 | modifier: 'action',
27 | icon: './arrow-left.svg',
28 | iconPos: 'before'
29 | },
30 | /*
31 | * Multiple scenario instances can be rendered in the same preview
32 | * by providing an array of props for the `preview` option.
33 | * Each preview props object will be merged with the default scenario
34 | * props defined above.
35 | */
36 | preview: [
37 | {
38 | text: 'Back'
39 | },
40 | {
41 | text: 'A prev button with a long label'
42 | }
43 | ]
44 | }
45 | ],
46 |
47 | /*
48 | * Configure the component's search behaviour.
49 | *
50 | * The `aliases` array is a list of extra strings to match against.
51 | * `hidden: true` can be used to prevent the component from
52 | * being matched in the search results.
53 | */
54 | search: {
55 | aliases: ['clicker']
56 | },
57 |
58 | /*
59 | * The component preview option lets users customise
60 | * previews on a per-component basis.
61 | */
62 | preview: {
63 | /*
64 | * The wrap() method allows wrapping of the entire rendered
65 | * output in arbitary HTML.
66 | */
67 | wrap(html, { scenario, component }) {
68 | return `
69 |
${component.label} / ${scenario.label}
70 | ${html}
71 | `;
72 | },
73 |
74 | /*
75 | * The wrapEach() method allows wrapping of each scenario instance
76 | * within the preview window. Only applicable when defining multiple
77 | * scenario preview instances such as in the `prev`scenario above.
78 | */
79 | wrapEach(html) {
80 | return `${html}
`;
81 | },
82 |
83 | /*
84 | * Preview-specific scripts can be added here.
85 | * These can also be specified globally instead of on
86 | * a per-component basis if required.
87 | */
88 | js: `console.log('This is one way to inject preview-specific JS');`,
89 |
90 | /*
91 | * Preview-specific styles can be added here.
92 | * These can also be specified globally instead of on
93 | * a per-component basis if required.
94 | */
95 | css: `
96 | .preview-title {
97 | color: #aaa;
98 | font-weight: normal;
99 | margin-bottom: 10px;
100 | }
101 | .preview-instance {
102 | margin-bottom: 10px;
103 | }
104 | `
105 | },
106 |
107 | /*
108 | * The notes property is used by the Notes plugin. Any text here will
109 | * be rendered in a custom notes panel in the inspector.
110 | */
111 | notes: `
112 | Some notes on the button component.
113 |
114 | These were added inline in the [button config file]({file:button/button.config.js})
115 | `
116 | };
117 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/01-units/button/button.js:
--------------------------------------------------------------------------------
1 | /* global document */
2 |
3 | const buttons = document.querySelectorAll('.button');
4 |
5 | function clickLogger(event) {
6 | console.log('button clicked!');
7 | event.preventDefault();
8 | }
9 |
10 | for (let i = 0; i < buttons.length; i++) {
11 | buttons[i].addEventListener('click', clickLogger);
12 | }
13 |
14 | // If using the styleguide bundler plugin with
15 | // Hot Module Replacement (HMR) enabled, the code needs to
16 | // handle cleaning up event listeners etc when the module
17 | // contents are replaced. See https://parceljs.org/hmr.html
18 | if (module.hot) {
19 | module.hot.dispose(() => {
20 | for (let i = 0; i < buttons.length; i++) {
21 | buttons[i].removeEventListener('click', clickLogger);
22 | }
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/01-units/button/button.scss:
--------------------------------------------------------------------------------
1 | .button {
2 | border: 1px solid #ccc;
3 | padding: 10px 10px;
4 | background: #f4f4f4;
5 | cursor: pointer;
6 | border-radius: 4px;
7 | display: inline-flex;
8 | align-items: center;
9 | color: #333;
10 | text-decoration: none;
11 |
12 | &__text {
13 | font-size: 16px;
14 | }
15 |
16 | &__icon {
17 | width: 14px;
18 | }
19 |
20 | &__text + &__icon {
21 | margin-left: 10px;
22 | }
23 |
24 | &__icon + &__text {
25 | margin-left: 10px;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/01-units/button/view.njk:
--------------------------------------------------------------------------------
1 |
2 | {% if icon and iconPos == 'before' %}
{% endif %}
3 | {{ text }}
4 | {% if icon and iconPos == 'after' %}
{% endif %}
5 |
6 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/01-units/figure/assets/placeholder.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frctl/fractalite/8e1dd708265df58921e9061d787e59dd3022d1d6/demos/nunjucks/src/components/01-units/figure/assets/placeholder.jpg
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/01-units/figure/figure.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | scenarios: [
3 | {
4 | name: 'image',
5 | props: {
6 | type: 'image',
7 | src: './assets/placeholder.jpg',
8 | alt: 'Image alt text'
9 | }
10 | },
11 | {
12 | name: 'image-with-caption',
13 | props: {
14 | type: 'image',
15 | src: './assets/placeholder.jpg',
16 | alt: 'Image alt text',
17 | caption: 'A caption for the figure'
18 | }
19 | },
20 | {
21 | name: 'video',
22 | props: {
23 | type: 'video',
24 | src: 'https://www.youtube.com/embed/ScMzIvxBSi4'
25 | }
26 | }
27 | ]
28 | };
29 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/01-units/figure/figure.scss:
--------------------------------------------------------------------------------
1 | .figure {
2 | max-width: 600px;
3 |
4 | &__image {
5 | display: block;
6 | margin-bottom: 10px;
7 | max-width: 100%;
8 | }
9 |
10 | &__caption {
11 | font-size: 15px;
12 | font-style: italic;
13 | color: #777;
14 | }
15 |
16 | &--video &__container {
17 | position: relative;
18 | padding-bottom: 56.25%; /* 16:9 */
19 | padding-top: 25px;
20 | height: 0;
21 | }
22 |
23 | &--video &__video {
24 | position: absolute;
25 | top: 0;
26 | left: 0;
27 | width: 100%;
28 | height: 100%;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/01-units/figure/view.njk:
--------------------------------------------------------------------------------
1 |
2 |
9 | {% if caption %}{{ caption }}{% endif %}
10 |
11 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/02-patterns/card-set/card-set.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | scenarios: [
3 | {
4 | name: 'triplet',
5 | props: {
6 | cardType: 'card/image',
7 | cards: [
8 | {
9 | content: 'First kitten'
10 | },
11 | {
12 | content: 'Second kitten'
13 | },
14 | {
15 | content: 'Third kitten'
16 | }
17 | ]
18 | }
19 | }
20 | ]
21 | };
22 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/02-patterns/card-set/card-set.scss:
--------------------------------------------------------------------------------
1 | .card-set {
2 | overflow: hidden;
3 |
4 | &__card {
5 | margin-bottom: 15px;
6 | }
7 |
8 | @media all and (min-width: 780px) {
9 | &__cards {
10 | display: flex;
11 | list-style: none;
12 | margin-left: -10px;
13 | margin-right: -10px;
14 | }
15 |
16 | &__card {
17 | padding: 0 10px;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/02-patterns/card-set/view.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% for card in cards %}
4 | -
5 | {% component cardType, card %}
6 |
7 | {% endfor %}
8 |
9 |
10 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/02-patterns/card/card.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | label: 'Card',
3 |
4 | scenarios: [
5 | {
6 | name: 'basic',
7 | props: {
8 | title: 'A card title',
9 | content: 'Some content for the card.
'
10 | }
11 | },
12 | {
13 | name: 'cta',
14 | label: 'Call to action',
15 | props: {
16 | title: 'Sign up now',
17 | content: 'Click the button below to do this thing.
',
18 | cta: {
19 | text: 'Click me'
20 | }
21 | }
22 | },
23 | {
24 | name: 'image',
25 | label: 'With image',
26 | props: {
27 | title: 'A kitten',
28 | content: 'Because every example needs a kitten in there somewhere.
',
29 | image: {
30 | src: 'http://placekitten.com/900/600',
31 | alt: 'Meow'
32 | }
33 | }
34 | }
35 | ]
36 | };
37 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/02-patterns/card/card.scss:
--------------------------------------------------------------------------------
1 | .card {
2 | border: 1px solid #ddd;
3 | padding: 15px;
4 | max-width: 600px;
5 | box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
6 |
7 | &__title {
8 | font-size: 20px;
9 | margin-bottom: 20px;
10 | }
11 |
12 | &__content + .button {
13 | margin-top: 20px;
14 | }
15 |
16 | &__image {
17 | width: 100%;
18 | }
19 |
20 | .figure + &__content {
21 | margin-top: 20px;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/02-patterns/card/notes.md:
--------------------------------------------------------------------------------
1 | This is a card component.
2 |
3 | It uses the [Button]({component:button}) component inside of it.
4 |
5 | This file is generated from the contents of the [notes.md]({file:card/notes.md}) file.
6 |
--------------------------------------------------------------------------------
/demos/nunjucks/src/components/02-patterns/card/view.njk:
--------------------------------------------------------------------------------
1 | {#
2 | # The Nunjucks adapter supports importing of child components
3 | # via the {% component %} tag.
4 | #
5 | # Some examples of how it can be used:
6 | #
7 | # {% component 'component-name' %}
8 | # {% component 'component-name/scenario-name' %}
9 | # {% component 'component-name', props %}
10 | # {% component 'component-name/scenario-name', props %}
11 | #
12 | # Note that `props` above can be a variable or an inline-object of property values.
13 | # If a scenario name *and* props are specified then the two objects are merged.
14 | #
15 | # In the example below, the 'button' component is rendered using the props from
16 | # the 'next' scenario merged with data from the `cta` object.
17 | #}
18 |
19 |
{{ title }}
20 | {% if image %}{% component 'figure/image', image %}{% endif %}
21 |
22 | {{ content | safe }}
23 |
24 | {% if cta %}{% component 'button/next', cta %}{% endif %}
25 |
26 |
--------------------------------------------------------------------------------
/demos/vue/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | src/assets/preview.js
3 | build
4 |
--------------------------------------------------------------------------------
/demos/vue/README.md:
--------------------------------------------------------------------------------
1 | ## Vue demo project
2 |
3 | A proof-of-concept demo using the Vue adapter and the asset bundler plugin.
4 |
--------------------------------------------------------------------------------
/demos/vue/fractal.config.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 | const vueAdapter = require('@frctl/fractalite-adapter-vue')({
3 | // Vue adapter opts here
4 | });
5 |
6 | module.exports = {
7 | title: 'Vue Demo',
8 |
9 | adapter: vueAdapter,
10 |
11 | components: resolve(__dirname, './src/components'),
12 |
13 | plugins: [
14 | require('@frctl/fractalite-plugin-assets-bundler')({
15 | entryBuilder: vueAdapter.entryBuilder, // Add Vue entry builder
16 | entryFile: resolve(__dirname, './src/assets/preview.js'),
17 | outFile: resolve(__dirname, './dist/assets/build.js')
18 | })
19 | ]
20 | };
21 |
--------------------------------------------------------------------------------
/demos/vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite-demo-vue",
3 | "private": true,
4 | "scripts": {
5 | "start": "fractalite start --port 3031",
6 | "build": "fractalite build --dest ./build --serve --port 4041",
7 | "serve-static": "fractalite serve --dest ./build --port 4041"
8 | },
9 | "devDependencies": {
10 | "@frctl/fractalite": "^0.0.0",
11 | "@frctl/fractalite-adapter-vue": "^0.0.0",
12 | "@frctl/fractalite-plugin-assets-bundler": "^0.0.0",
13 | "@vue/component-compiler-utils": "^2.5.2",
14 | "vue-template-compiler": "^2.6.6"
15 | },
16 | "dependencies": {
17 | "vue": "^2.6.6",
18 | "vue-hot-reload-api": "^2.3.2"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/demos/vue/src/components/counter/counter.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | scenarios: [
3 | {
4 | name: 'Standard',
5 | props: {
6 | label: 'Click here to increment the counter'
7 | }
8 | }
9 | ]
10 | };
11 |
--------------------------------------------------------------------------------
/demos/vue/src/components/counter/counter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ label }}: {{ counter }}
4 |
5 |
6 |
7 |
23 |
24 |
37 |
--------------------------------------------------------------------------------
/demos/vue/src/components/multi-counter/multi-counter.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | scenarios: [
3 | {
4 | name: 'default',
5 | props: {
6 | 'total-label': 'Sum of A and B',
7 | counters: [
8 | {
9 | label: 'A'
10 | },
11 | {
12 | label: 'B'
13 | }
14 | ]
15 | }
16 | }
17 | ]
18 | };
19 |
--------------------------------------------------------------------------------
/demos/vue/src/components/multi-counter/multi-counter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
{{ totalLabel || 'Total' }}: {{ total }}
11 |
12 |
13 |
14 |
29 |
30 |
46 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transform: {}
3 | };
4 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": ["packages/**", "demos/**"],
3 | "version": "0.0.0"
4 | }
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "devDependencies": {
4 | "@frctl/eslint-config-frctl": "^0.1.3",
5 | "asyncro": "^3.0.0",
6 | "common-tags": "^1.8.0",
7 | "fs-extra": "^8.1.0",
8 | "jest": "^24.9.0",
9 | "lerna": "^3.20.2",
10 | "xo": "^0.25.3"
11 | },
12 | "name": "fractalite",
13 | "scripts": {
14 | "bootstrap": "lerna bootstrap",
15 | "lint:fix": "xo --fix",
16 | "test": "xo && jest --coverage",
17 | "test:watch": "xo && jest --watch",
18 | "test:ci": "jest",
19 | "test:generate:components": "node ./test/helpers/generate-components",
20 | "fractalite:build": "cd packages/fractalite && npm run build",
21 | "fractalite:dev": "cd packages/fractalite && npm run dev",
22 | "demo:vue": "npm run bootstrap && cd demos/vue && npm run start",
23 | "demo:nunjucks": "npm run bootstrap && cd demos/nunjucks && npm run start",
24 | "demo:nunjucks:build": "npm run bootstrap && cd demos/nunjucks && npm run build",
25 | "demo": "npm run demo:nunjucks",
26 | "demo:build": "npm run demo:nunjucks:build"
27 | },
28 | "xo": {
29 | "extends": "@frctl/eslint-config-frctl",
30 | "prettier": true,
31 | "spaces": true,
32 | "rules": {
33 | "unicorn/catch-error-name": [
34 | "error",
35 | {
36 | "name": "err"
37 | }
38 | ]
39 | },
40 | "ignores": [
41 | "**/dist/*",
42 | "**/assets/*",
43 | "**/test/**",
44 | "**/*.test.js",
45 | "./demos/**"
46 | ]
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/adapter-nunjucks/README.md:
--------------------------------------------------------------------------------
1 | ## @frctl/fractalite-adapter-nunjucks
2 |
3 | A Nunjucks template engine adapter for the Fractalite prototype.
4 |
5 | ### Installation
6 |
7 | `npm i @frctl/fractalite-adapter-nunjucks`
8 |
9 | Specify as adapter in the project config file:
10 |
11 | ```js
12 | // fractal.config.js
13 | module.exports = {
14 | adapter: require('@frctl/fractalite-adapter-nunjucks')({
15 | // config options here
16 | })
17 | };
18 | ```
19 |
20 | ### Usage
21 |
22 | A Nunjucks view template should be created per component. This should be called `view.njk` or `{component-name}.view.njk`.
23 |
24 | ```
25 | @button
26 | ├── button.config.js
27 | └── view.njk
28 | ```
29 |
30 | ```html
31 |
32 |
33 | {{ text }}
34 |
35 | ```
36 |
37 | #### Including child components
38 |
39 | The Nunjucks adapter supports importing of child components in templates via the `{% component %}` tag.
40 |
41 | ```html
42 | {% component 'component-name' %}
43 | {% component 'component-name/scenario-name' %}
44 | {% component 'component-name', props %}
45 | {% component 'component-name/scenario-name', props %}
46 | ```
47 |
48 | Note that `props` above can be a variable or an inline-object of property values.
49 |
50 | If a scenario name *and* props are specified then the two objects are merged.
51 |
--------------------------------------------------------------------------------
/packages/adapter-nunjucks/index.js:
--------------------------------------------------------------------------------
1 | const { defaultsDeep } = require('@frctl/fractalite-support/utils');
2 | const createAdapter = require('./src/adapter');
3 |
4 | const defaults = {
5 | views: ['view.njk', '{name}.view.njk']
6 | };
7 |
8 | module.exports = function(opts = {}) {
9 | opts = defaultsDeep(opts, defaults);
10 |
11 | return function nunjucksAdapter(app, compiler) {
12 | const adapter = createAdapter(compiler, opts);
13 |
14 | // Compiler middleware to identify .njk files as HTML fragments so that
15 | // further pre-processing of templates can take place later if required.
16 | compiler.use(components => {
17 | components.forEach(component => {
18 | const nunjucksFiles = component.matchFiles(['*.njk']);
19 | nunjucksFiles.forEach(file => {
20 | file.isHTMLFragment = true;
21 | });
22 | });
23 | });
24 |
25 | return adapter;
26 | };
27 | };
28 |
--------------------------------------------------------------------------------
/packages/adapter-nunjucks/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite-adapter-nunjucks",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@frctl/fractalite-core": "^0.0.0",
6 | "@frctl/fractalite-support": "^0.0.0",
7 | "nunjucks": "^3.1.3"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/adapter-nunjucks/src/adapter.js:
--------------------------------------------------------------------------------
1 | const nunjucksEnv = require('./env');
2 |
3 | module.exports = function(compiler, opts = {}) {
4 | const env = nunjucksEnv(opts);
5 | const { views } = env.loader;
6 | env.state = compiler.getState(); // For use in extensions/filters etc
7 |
8 | compiler.on('finish', state => {
9 | /*
10 | * Empty views array and then replace with
11 | * updated set of view tempates.
12 | */
13 | views.length = 0;
14 | state.components.forEach(component => {
15 | // Lookup view by handle: {% include 'button' %}
16 | views.push({
17 | name: component.name,
18 | getContents: () => {
19 | const view = component.matchFiles(opts.views)[0];
20 | return view ? view.getContents() : null;
21 | }
22 | });
23 | // Lookup component-relative file by `./filename.ext` path
24 | component.files.forEach(file => {
25 | views.push({
26 | name: `./${file.componentPath}`,
27 | getContents: () => file.getContents()
28 | });
29 | });
30 | });
31 | });
32 |
33 | async function render(component, props, state) {
34 | return new Promise((resolve, reject) => {
35 | env.render(component.name, props, (err, result) => {
36 | return err ? reject(err) : resolve(result);
37 | });
38 | });
39 | }
40 |
41 | async function getTemplateString(component) {
42 | const view = component.matchFiles(opts.views)[0];
43 | return view ? view.getContents() : null;
44 | }
45 |
46 | return { render, getTemplateString };
47 | };
48 |
--------------------------------------------------------------------------------
/packages/adapter-nunjucks/src/env.js:
--------------------------------------------------------------------------------
1 | const { Environment } = require('nunjucks');
2 | const ComponentExtension = require('./extension-component');
3 | const Loader = require('./loader');
4 |
5 | module.exports = function nunjucks(config = {}) {
6 | const loader = new Loader();
7 | const env = new Environment([loader]);
8 |
9 | env.addExtension('component', new ComponentExtension());
10 |
11 | Object.keys(config.globals || {}).forEach(key => env.addGlobal(key, config.globals[key]));
12 | Object.keys(config.extensions || {}).forEach(key => env.addExtension(key, config.extensions[key]));
13 | if (Array.isArray(config.filters)) {
14 | config.filters.forEach(filter => env.addFilter(filter.name, filter.filter, filter.async));
15 | } else {
16 | Object.keys(config.filters || {}).forEach(key => env.addFilter(key, config.filters[key], config.filters[key].async));
17 | }
18 |
19 | env.loader = loader;
20 |
21 | return env;
22 | };
23 |
--------------------------------------------------------------------------------
/packages/adapter-nunjucks/src/extension-component.js:
--------------------------------------------------------------------------------
1 | const { SafeString } = require('nunjucks').runtime;
2 | const { mergeProps, getComponent, getScenario } = require('@frctl/fractalite-core/helpers');
3 |
4 | module.exports = class ComponentExtension {
5 | constructor() {
6 | this.tags = ['component'];
7 | }
8 |
9 | parse(parser, nodes) {
10 | const tok = parser.nextToken();
11 | const args = parser.parseSignature(null, true);
12 |
13 | parser.advanceAfterBlockEnd(tok.value);
14 | return new nodes.CallExtensionAsync(this, 'run', args, null);
15 | }
16 |
17 | run(nunjucksContext, handle, ...args) {
18 | const { env } = nunjucksContext;
19 | const callback = args.pop();
20 | let props = args.shift() || {};
21 | const shouldMerge = args.shift();
22 | const { state } = env;
23 |
24 | const [componentName, scenarioName] = handle.split('/');
25 |
26 | const component = getComponent(state, componentName, true);
27 | const scenario = getScenario(component, scenarioName, true);
28 |
29 | props = shouldMerge === false ? props : mergeProps(state, scenario.props, props);
30 |
31 | env.render(component.name, props, (err, result) => {
32 | if (err) {
33 | return callback(err);
34 | }
35 | callback(null, new SafeString(result));
36 | });
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/packages/adapter-nunjucks/src/loader.js:
--------------------------------------------------------------------------------
1 | const { Loader } = require('nunjucks');
2 |
3 | module.exports = Loader.extend({
4 | views: [],
5 | async: true,
6 | async getSource(lookup, callback) {
7 | const name = lookup.replace(/^@/, '');
8 | const view = this.views.find(view => view.name === name);
9 | if (view) {
10 | try {
11 | const contents = await view.getContents();
12 | return callback(null, {
13 | src: contents,
14 | path: lookup,
15 | noCache: true
16 | });
17 | } catch (err) {
18 | return callback(err);
19 | }
20 | }
21 | callback(new Error(`Nunjucks adapter: Template '${lookup}' not found`));
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/packages/adapter-vue/index.js:
--------------------------------------------------------------------------------
1 | const { relative } = require('path');
2 | const { merge, camelCase } = require('lodash');
3 | const { source } = require('common-tags');
4 | const createAdapter = require('./src/adapter');
5 |
6 | const defaults = {
7 | previewAppId: 'prevue'
8 | };
9 |
10 | module.exports = function(opts = {}) {
11 | opts = merge(defaults, opts);
12 | const adapter = createAdapter(opts);
13 |
14 | function attacher(app) {
15 | return adapter;
16 | }
17 |
18 | attacher.entryBuilder = function(state, { dir }) {
19 | const imports = state.components.map(component => {
20 | const vueFile = component.files.find(f => f.ext === '.vue');
21 | const as = camelCase(component.name);
22 | const path = relative(dir, vueFile.path);
23 | return { as, name: component.name, path };
24 | });
25 |
26 | return source`
27 | import Vue from 'vue/dist/vue.js';
28 |
29 | ${opts.setup}
30 |
31 | ${imports.map(
32 | ({ name, as, path }) => source`
33 | import ${as} from './${path}';
34 | Vue.component('${name}', ${as});
35 | `
36 | )}
37 |
38 | new Vue({ el: '#${opts.previewAppId}' });
39 | `;
40 | };
41 |
42 | return attacher;
43 | };
44 |
--------------------------------------------------------------------------------
/packages/adapter-vue/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite-adapter-vue",
3 | "version": "0.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "common-tags": {
8 | "version": "1.8.0",
9 | "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz",
10 | "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw=="
11 | },
12 | "he": {
13 | "version": "1.1.1",
14 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
15 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0="
16 | },
17 | "json5": {
18 | "version": "2.1.0",
19 | "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz",
20 | "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==",
21 | "requires": {
22 | "minimist": "^1.2.0"
23 | }
24 | },
25 | "lodash": {
26 | "version": "4.17.11",
27 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
28 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
29 | },
30 | "minimist": {
31 | "version": "1.2.0",
32 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
33 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
34 | },
35 | "node-html-parser": {
36 | "version": "1.1.14",
37 | "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.1.14.tgz",
38 | "integrity": "sha512-UcvX5vo3vqlDUpOVvy67Qdp8um0cYc30blTI1yLArF7g8SD3Ci1xomIkPBg1+AKZ8LgrYixvOQ9G2PkOFk2AEg==",
39 | "requires": {
40 | "he": "1.1.1"
41 | }
42 | },
43 | "strip-indent": {
44 | "version": "2.0.0",
45 | "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz",
46 | "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g="
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/packages/adapter-vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite-adapter-vue",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "common-tags": "^1.8.0",
6 | "json5": "^2.1.0",
7 | "lodash": "^4.17.10",
8 | "node-html-parser": "^1.1.14",
9 | "strip-indent": "^2.0.0"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/adapter-vue/src/adapter.js:
--------------------------------------------------------------------------------
1 | const JSON5 = require('json5');
2 | const stripIndent = require('strip-indent');
3 | const { parse } = require('node-html-parser');
4 | const { map, isPlainObject, isString } = require('lodash');
5 |
6 | module.exports = function(opts = {}) {
7 | return {
8 | async render(component, props, state) {
9 | const attrs = map(props, toVueAttr);
10 | return `
11 | <${component.name} ${attrs.join('\n')}>${component.name}>
12 | `;
13 | },
14 |
15 | getPreviewString(content) {
16 | return `${content}
`;
17 | },
18 |
19 | async getTemplateString(component) {
20 | const vueFile = component.files.find(f => f.ext === '.vue');
21 | const contents = vueFile ? await vueFile.getContents() : null;
22 | if (contents) {
23 | const root = parse(contents);
24 | const tpl = root.querySelector('template');
25 | return tpl ? stripIndent(tpl.innerHTML) : null;
26 | }
27 | return null;
28 | }
29 | };
30 | };
31 |
32 | function toVueAttr(value, key) {
33 | if (isPlainObject(value) || Array.isArray(value)) {
34 | return `:${key}="${JSON5.stringify(value, { quote: "'" })}"`;
35 | }
36 | if (isString(value)) {
37 | return `${key}="${value}"`;
38 | }
39 | // TODO: handle Date instances and other objects...
40 | return `:${key}="${value}"`;
41 | }
42 |
--------------------------------------------------------------------------------
/packages/app/README.md:
--------------------------------------------------------------------------------
1 | ## @frctl/fractalite-app
2 |
3 | A web framework for building Fractalite-based applications.
4 |
5 | Used to create the main Fractalite styleguide UI.
6 |
--------------------------------------------------------------------------------
/packages/app/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const jsonErrors = require('koa-json-error');
3 | const prettier = require('prettier');
4 | const cleanStack = require('clean-stacktrace');
5 | const relativePaths = require('clean-stacktrace-relative-paths');
6 | const { getComponent, isFile } = require('@frctl/fractalite-core/helpers');
7 | const serveStatic = require('./src/serve');
8 | const App = require('./src/app');
9 |
10 | module.exports = function(compiler, opts = {}) {
11 | const app = new App(compiler, opts);
12 | const { router, views, socket } = app;
13 |
14 | app.use(jsonErrors());
15 |
16 | app.use(async (ctx, next) => {
17 | if (ctx.path.startsWith('/api')) return next();
18 | try {
19 | await next();
20 | const status = ctx.status || 404;
21 | if (status === 404) {
22 | ctx.throw(404, 'Page not found');
23 | }
24 | } catch (err) {
25 | ctx.error = err;
26 | ctx.state.error = err;
27 | err.path = ctx.path;
28 | ctx.status = err.status || 500;
29 | ctx.app.emit('error', err, ctx);
30 | try {
31 | return ctx.render('error');
32 | } catch (renderError) {
33 | ctx.body = err;
34 | }
35 | }
36 | });
37 |
38 | app.use(async (ctx, next) => {
39 | try {
40 | await next();
41 | } catch (err) {
42 | err.stack = cleanStack(err.stack, relativePaths());
43 | throw err;
44 | }
45 | });
46 |
47 | app.on('error', err =>
48 | socket.broadcast('err', {
49 | name: err.name,
50 | message: err.message,
51 | stack: cleanStack(err.stack, relativePaths()),
52 | status: err.status
53 | })
54 | );
55 |
56 | app.on('state.updated', (...results) => socket.broadcast('state.updated'));
57 |
58 | /*
59 | * View rendering middleware
60 | */
61 | app.use((ctx, next) => {
62 | ctx.response.render = async function(path, locals = {}, opts) {
63 | const state = { ...ctx.state, ...locals };
64 | ctx.type = ctx.type || 'text/html';
65 | ctx.body = await views.renderAsync(path, state, opts);
66 | };
67 |
68 | ctx.response.renderString = async function(str, locals = {}, opts) {
69 | const state = { ...ctx.state, ...locals };
70 | ctx.type = ctx.type || 'text/html';
71 | ctx.body = await views.renderStringAsync(str, state, opts);
72 | };
73 |
74 | ctx.loadView = path => views.getTemplateAsync(path);
75 |
76 | ctx.render = ctx.response.render;
77 | ctx.renderString = ctx.response.renderString;
78 |
79 | return next();
80 | });
81 |
82 | /*
83 | * File sending middleware
84 | */
85 | app.use((ctx, next) => {
86 | ctx.response.sendFile = async function(file) {
87 | if (!isFile(file)) {
88 | throw new Error('Only Files can be sent');
89 | }
90 | const contents = await file.getContents();
91 | ctx.lastModified = file.stats.mtime;
92 | ctx.length = contents.length;
93 | ctx.type = file.ext;
94 | ctx.type = ctx.response.type || 'text/plain';
95 | if (app.mode === 'develop') {
96 | ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate');
97 | ctx.set('Pragma', 'no-cache');
98 | ctx.set('Expires', 0);
99 | }
100 | ctx.body = fs.createReadStream(file.path);
101 | };
102 |
103 | ctx.sendFile = ctx.response.sendFile;
104 | return next();
105 | });
106 |
107 | /*
108 | * Middleware to add useful properties to state object
109 | * so that they will be available to templates.
110 | */
111 | app.use((ctx, next) => {
112 | ctx.state.mode = ctx.mode;
113 | ctx.state.error = ctx.error;
114 | ctx.state.request = {
115 | params: ctx.params,
116 | path: ctx.path,
117 | url: ctx.url,
118 | route: ctx.route
119 | };
120 | const compilerState = app.compiler.getState();
121 | Object.keys(compilerState).forEach(key => {
122 | ctx[key] = compilerState[key];
123 | ctx.state[key] = ctx[key];
124 | });
125 | return next();
126 | });
127 |
128 | /*
129 | * Add a route parameter loader for the :component param.
130 | */
131 | router.param('component', (name, ctx, next) => {
132 | if (!name) return next();
133 | try {
134 | ctx.component = getComponent(ctx.state, name, true);
135 | } catch (err) {
136 | ctx.throw(404, err);
137 | }
138 | ctx.state.component = ctx.component;
139 | return next();
140 | });
141 |
142 | views.addGlobal('url', (name, params) => app.url(name, params));
143 | views.addGlobal('resourceUrl', (name, path) => app.resourceUrl(name, path));
144 |
145 | app.addRoute('file', '/src/:file(.+)', ctx => {
146 | const file = ctx.files.find(f => f.relative === ctx.params.file);
147 | if (file) {
148 | return ctx.sendFile(file);
149 | }
150 | ctx.throw(404, 'File not found');
151 | });
152 |
153 | app.addBuilder((state, { copy }) => {
154 | state.files.forEach(file =>
155 | copy(file.path, {
156 | name: 'file',
157 | params: { file }
158 | })
159 | );
160 | });
161 |
162 | /*
163 | * Compiler middleware to add url properties to files
164 | */
165 | app.compiler.use(async (components, next, { files }) => {
166 | await next();
167 | files.forEach(file => {
168 | file.url = app.url('file', { file });
169 | });
170 | });
171 |
172 | app.addRoute('app-css', `/app/assets/bundle.css`, ctx => {
173 | ctx.type = 'text/css';
174 | ctx.body = prettier.format(app.getCSS(), { parser: 'css' });
175 | });
176 |
177 | app.addRoute('app-js', `/app/assets/bundle.js`, ctx => {
178 | ctx.type = 'application/javascript';
179 | ctx.body = prettier.format(app.getJS(), { parser: 'babel' });
180 | });
181 |
182 | app.addBuilder((state, { request }) => request({ name: 'app-css' }));
183 | app.addBuilder((state, { request }) => request({ name: 'app-js' }));
184 |
185 | app.use((ctx, next) => {
186 | ctx.state.scripts = app.getScripts();
187 | ctx.state.stylesheets = app.getStylesheets();
188 | if (app.getCSS() !== '') ctx.state.stylesheets.push(app.url('app-css'));
189 | if (app.getJS() !== '') ctx.state.scripts.push(app.url('app-js'));
190 | return next();
191 | });
192 |
193 | return app;
194 | };
195 |
196 | module.exports.App = App;
197 | module.exports.serveStatic = serveStatic;
198 |
--------------------------------------------------------------------------------
/packages/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite-app",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@frctl/fractalite-core": "^0.0.0",
6 | "@frctl/fractalite-support": "^0.0.0",
7 | "asyncro": "^3.0.0",
8 | "axios": "^0.18.0",
9 | "clean-stacktrace": "^1.1.0",
10 | "clean-stacktrace-relative-paths": "^1.0.4",
11 | "cp-file": "^6.0.0",
12 | "del": "^3.0.0",
13 | "eventemitter2": "^5.0.1",
14 | "fs-extra": "^7.0.0",
15 | "get-port": "^4.0.0",
16 | "globby": "^8.0.1",
17 | "koa": "^2.5.2",
18 | "koa-compose": "^4.1.0",
19 | "koa-compress": "^3.0.0",
20 | "koa-json-error": "^3.1.2",
21 | "koa-mount": "^3.0.0",
22 | "koa-router": "7.4.0",
23 | "koa-send": "^5.0.0",
24 | "koa-socket-2": "^1.0.17",
25 | "koa-static": "^5.0.0",
26 | "lodash": "^4.17.10",
27 | "nunjucks": "^3.1.3",
28 | "p-throttle": "^3.0.0",
29 | "prettier": "^1.16.4"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/app/src/builder.js:
--------------------------------------------------------------------------------
1 | const { join } = require('path');
2 | const https = require('https');
3 | const { outputFile } = require('fs-extra');
4 | const { map } = require('asyncro');
5 | const { normalizePath } = require('@frctl/fractalite-support/utils');
6 | const cpFile = require('cp-file');
7 | const axios = require('axios');
8 | const del = require('del');
9 | const throttle = require('p-throttle');
10 |
11 | module.exports = function() {
12 | const copyTasks = [];
13 | const requestTasks = [];
14 | const builder = {};
15 |
16 | builder.addCopyTask = task => {
17 | copyTasks.push(task);
18 | };
19 |
20 | builder.addRequestTask = task => {
21 | requestTasks.push(task);
22 | };
23 |
24 | builder.run = async (server, opts = {}) => {
25 | if (!opts.dest) {
26 | throw new Error(`You must specify a build destination`);
27 | }
28 | const dest = normalizePath(opts.dest);
29 |
30 | if (opts.clean === true) {
31 | await del(dest);
32 | }
33 |
34 | const { port } = server.address();
35 | const protocol = server instanceof https.Server ? 'https' : 'http';
36 | const address = `${protocol}://${opts.hostname || '127.0.0.1'}:${port}`;
37 |
38 | const copies = map(copyTasks, async task => {
39 | const to = join(dest, task.to);
40 | await cpFile(task.from, to);
41 | return { ...task, to };
42 | });
43 |
44 | const throttledRequest = throttle(
45 | async task => {
46 | try {
47 | const to = join(dest, task.to);
48 | const { data } = await axios.get(`${address}${task.from}`);
49 | const contents = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
50 | await outputFile(to, contents);
51 | return { ...task, to };
52 | } catch (err) {
53 | // TODO: throw more descriptive error with URL
54 | throw err;
55 | }
56 | },
57 | opts.requestLimit || 100,
58 | 1000
59 | );
60 |
61 | const requests = map(requestTasks, throttledRequest);
62 |
63 | const [copyResults, requestResults] = await Promise.all([copies, requests]);
64 |
65 | return [...copyResults, ...requestResults];
66 | };
67 |
68 | return builder;
69 | };
70 |
--------------------------------------------------------------------------------
/packages/app/src/include-raw.js:
--------------------------------------------------------------------------------
1 | const { SafeString } = require('nunjucks').runtime;
2 |
3 | module.exports = class IncludeRawExtension {
4 | constructor() {
5 | this.tags = ['includeraw'];
6 | }
7 |
8 | parse(parser, nodes) {
9 | const tok = parser.nextToken();
10 | const args = parser.parseSignature(null, true);
11 |
12 | parser.advanceAfterBlockEnd(tok.value);
13 | return new nodes.CallExtensionAsync(this, 'run', args, null);
14 | }
15 |
16 | run(ctx, name, ...args) {
17 | const { env } = ctx;
18 | const done = args.pop();
19 |
20 | env.getTemplate(name, false, (err, tpl) => {
21 | if (err) return done(err);
22 | done(null, new SafeString(tpl.tmplStr));
23 | });
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/packages/app/src/mode.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('lodash');
2 |
3 | module.exports = function(opts) {
4 | const modes = [
5 | {
6 | mode: 'develop',
7 | hostname: 'localhost',
8 | port: 3000,
9 | cache: false,
10 | paths: {},
11 | permalinks: {
12 | prefix: null,
13 | indexes: false,
14 | ext: false
15 | }
16 | },
17 | {
18 | mode: 'build',
19 | dest: null,
20 | clean: false,
21 | gitignore: false,
22 | hostname: 'localhost',
23 | cache: true,
24 | paths: {},
25 | permalinks: {
26 | prefix: null,
27 | indexes: true,
28 | ext: '.html'
29 | }
30 | }
31 | ];
32 |
33 | opts = typeof opts === 'string' ? { opts } : opts;
34 | const mode = modes.find(m => m.mode === opts.mode);
35 | if (!mode) {
36 | throw new Error(`Invalid mode '${opts.mode}'`);
37 | }
38 |
39 | const modeOpts = merge(mode, opts, {
40 | toString() {
41 | return this.mode;
42 | }
43 | });
44 |
45 | mode.paths = merge({}, mode.permalinks, mode.paths);
46 | mode.name = mode.mode;
47 |
48 | return modeOpts;
49 | };
50 |
--------------------------------------------------------------------------------
/packages/app/src/resources.js:
--------------------------------------------------------------------------------
1 | const { join, relative } = require('path');
2 | const { flatten } = require('lodash');
3 | const serve = require('koa-static');
4 | const mount = require('koa-mount');
5 | const compose = require('koa-compose');
6 | const globby = require('globby');
7 | const { normalizePath } = require('@frctl/fractalite-support/utils');
8 |
9 | module.exports = function(opts = {}) {
10 | const sources = [];
11 |
12 | function routes() {
13 | return compose(
14 | sources.map(source => {
15 | const server = serve(source.path);
16 | return source.mount ? mount(source.mount, server) : server;
17 | })
18 | );
19 | }
20 |
21 | async function list() {
22 | // TODO: list all assets
23 | return flatten(
24 | await Promise.all(
25 | sources.map(async source => {
26 | const files = await globby([source.path, '!**/node_modules'], {
27 | onlyFiles: true,
28 | gitignore: false
29 | });
30 | return files.map(file => {
31 | const relPath = relative(source.path, file);
32 | return {
33 | path: file,
34 | url: url(`${source.name}:${relPath}`)
35 | };
36 | });
37 | })
38 | )
39 | );
40 | }
41 |
42 | function add(name, path, mount) {
43 | sources.push({ name, path: normalizePath(path), mount });
44 | }
45 |
46 | function url(path) {
47 | if (path.indexOf('://') !== -1 || path.startsWith('/')) {
48 | return path; // ignore external or absolute paths
49 | }
50 | const [sourceName, relativePath] = path.split(':');
51 | if (relativePath) {
52 | const source = sources.find(src => src.name === sourceName);
53 | if (!source) {
54 | throw new Error(`'${sourceName}' is not a recognised static asset source`);
55 | }
56 | return join(source.mount || '/', relativePath);
57 | }
58 | return path;
59 | }
60 |
61 | return { add, routes, url, list };
62 | };
63 |
--------------------------------------------------------------------------------
/packages/app/src/router.js:
--------------------------------------------------------------------------------
1 | const Router = require('koa-router');
2 |
3 | module.exports = function() {
4 | const router = new Router();
5 |
6 | router.add = function(route) {
7 | const { name, url, handler } = route;
8 |
9 | // Decorate the callback
10 | const callback = function(ctx, next) {
11 | ctx.route = route;
12 | return handler ? handler(ctx, next) : next();
13 | };
14 |
15 | router.get(name, url, callback);
16 | };
17 |
18 | return router;
19 | };
20 |
--------------------------------------------------------------------------------
/packages/app/src/serve.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const serve = require('koa-static');
3 | const send = require('koa-send');
4 | const { normalizePath } = require('@frctl/fractalite-support/utils');
5 |
6 | module.exports = function(dir, port = 0) {
7 | const serveOpts = {
8 | root: normalizePath(dir)
9 | };
10 |
11 | const app = new Koa();
12 | app.use(serve(serveOpts.root, serveOpts));
13 | app.use(ctx => send(ctx, 'index.html', serveOpts)); // File not found, serve index
14 |
15 | return new Promise((resolve, reject) => {
16 | const httpServer = app.listen(port, err => (err ? reject(err) : resolve(httpServer)));
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/packages/app/src/views-loader.js:
--------------------------------------------------------------------------------
1 | const { extname } = require('path');
2 | const { FileSystemLoader } = require('nunjucks');
3 |
4 | class ViewLoader extends FileSystemLoader {
5 | getSource(name) {
6 | if (!extname(name)) {
7 | name += '.njk';
8 | }
9 | return super.getSource(name);
10 | }
11 | }
12 |
13 | module.exports = ViewLoader;
14 |
--------------------------------------------------------------------------------
/packages/app/src/views.js:
--------------------------------------------------------------------------------
1 | const { normalize } = require('path');
2 | const { Environment } = require('nunjucks');
3 | const { SafeString } = require('nunjucks').runtime;
4 | const { each, merge, isPlainObject } = require('lodash');
5 | const ViewLoader = require('./views-loader');
6 | const IncludeRawExtension = require('./include-raw');
7 |
8 | const defaults = {
9 | paths: [],
10 | filters: {},
11 | globals: {},
12 | extensions: {},
13 | opts: {
14 | autoescape: true,
15 | throwOnUndefined: false,
16 | trimBlocks: false,
17 | lstripBlocks: false,
18 | noCache: true
19 | }
20 | };
21 |
22 | module.exports = function(config = {}) {
23 | config = merge({}, defaults, config);
24 |
25 | const loader = new ViewLoader(config.paths, {
26 | noCache: !config.cache
27 | });
28 |
29 | const njk = new Environment(loader, config.opts);
30 |
31 | const oldAddFilter = njk.addFilter;
32 | njk.addFilter = function(name, filter) {
33 | const wrapped = function(...args) {
34 | const done = args.pop();
35 | Promise.resolve(filter.bind(this)(...args))
36 | .then(result => done(null, result))
37 | .catch(err => done(err));
38 | };
39 | oldAddFilter.call(njk, name, wrapped, true);
40 | return njk;
41 | };
42 |
43 | each(config.filters, (filter, name) => njk.addFilter(name, filter));
44 | each(config.globals, (val, name) => njk.addGlobal(name, val));
45 | each(config.extensions, (ext, name) => njk.addExtension(name, ext));
46 |
47 | njk.loader = loader;
48 |
49 | njk.addPath = function(path) {
50 | if (loader.searchPaths.length === 1 && loader.searchPaths[0] === '.') {
51 | loader.searchPaths[0] = normalize(path);
52 | } else {
53 | loader.searchPaths.unshift(path);
54 | }
55 | };
56 |
57 | njk.mergeGlobal = function(name, val) {
58 | let current;
59 | try {
60 | current = njk.getGlobal(name);
61 | } catch (err) {
62 | // ignore
63 | }
64 | if (isPlainObject(current)) {
65 | return njk.addGlobal(name, merge({}, current, val));
66 | }
67 | return njk.addGlobal(name, val);
68 | };
69 |
70 | ['render', 'renderString', 'getTemplate'].forEach(method => {
71 | njk[`${method}Async`] = function(...args) {
72 | args = args.filter(arg => arg !== undefined);
73 | return new Promise((resolve, reject) => {
74 | njk[method](...args, (err, result) => {
75 | if (err) {
76 | return reject(err);
77 | }
78 | resolve(result);
79 | });
80 | });
81 | };
82 | });
83 |
84 | njk.addFilter('await', async (promise, catchError) => {
85 | try {
86 | return await promise;
87 | } catch (err) {
88 | if (catchError) {
89 | return err;
90 | }
91 | }
92 | });
93 |
94 | njk.SafeString = SafeString;
95 |
96 | njk.addExtension('includeraw', new IncludeRawExtension());
97 |
98 | return njk;
99 | };
100 |
--------------------------------------------------------------------------------
/packages/core/README.md:
--------------------------------------------------------------------------------
1 | ## @frctl/fractalite-core
2 |
3 | Core compiler, middleware and renderer modules for the Fractalite prototype.
4 |
--------------------------------------------------------------------------------
/packages/core/helpers.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./src/helpers');
2 |
--------------------------------------------------------------------------------
/packages/core/index.js:
--------------------------------------------------------------------------------
1 | const { get, isString } = require('lodash');
2 | const { normalizeSrc } = require('@frctl/fractalite-support/utils');
3 | const createCompiler = require('./src/compiler');
4 | const coreMiddleware = require('./src/middleware');
5 |
6 | function init(config = {}) {
7 | if (isString(config)) {
8 | config = {
9 | src: config,
10 | watch: {}
11 | };
12 | }
13 |
14 | const normalizedConfig = {
15 | src: normalizeSrc(config.src || {}),
16 | watch: normalizeSrc(config.watch || {}),
17 | opts: config.opts || {}
18 | };
19 |
20 | const compiler = createCompiler(normalizedConfig);
21 |
22 | coreMiddleware.forEach(({ key, handler }) => {
23 | const opts = get(normalizedConfig, `opts.${key}`, {});
24 | compiler.use(handler(opts));
25 | });
26 |
27 | return compiler;
28 | }
29 |
30 | module.exports = init;
31 | module.exports.createCompiler = init;
32 | module.exports.createAdapter = require('./src/create-adapter');
33 | module.exports.htmlAdapter = require('./src/html-adapter');
34 | module.exports.File = require('./src/entities/file');
35 | module.exports.Component = require('./src/entities/component');
36 | module.exports.Entity = require('./src/entities/entity');
37 | module.exports.middleware = require('./src/middleware');
38 | module.exports.read = require('./src/read');
39 | module.exports.watch = require('chokidar').watch;
40 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite-core",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@frctl/fractalite-support": "^0.0.0",
6 | "deep-freeze": "^0.0.1",
7 | "enhanced-require": "^0.5.0-beta6",
8 | "lodash": "^4.17.10",
9 | "common-tags": "^1.8.0",
10 | "chokidar": "^2.0.4",
11 | "fs-extra": "^7.0.0",
12 | "lodash": "^4.17.10",
13 | "multimatch": "^2.1.0",
14 | "pupa": "^1.0.0",
15 | "glob-base": "^0.3.0",
16 | "globby": "^8.0.1",
17 | "slash": "^2.0.0",
18 | "asyncro": "^3.0.0"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/core/src/compile-components.js:
--------------------------------------------------------------------------------
1 | const { orderBy, difference, compact, clone } = require('lodash');
2 | const Component = require('./entities/component');
3 | const defaultComponentMatcher = require('./component-matcher');
4 |
5 | module.exports = async function(files, opts = {}) {
6 | files = orderBy(files, 'path.length', 'desc'); // Work from deepest to shallowest
7 | const dirs = files.filter(f => f.stats.isDirectory());
8 | let unusedFiles = files.filter(f => f.stats.isFile());
9 |
10 | const matcher = opts.matcher || defaultComponentMatcher;
11 |
12 | const components = dirs.map(dir => {
13 | const children = unusedFiles.filter(f => f.path.startsWith(`${dir.path}/`));
14 |
15 | if (!matcher(dir, children, opts)) {
16 | return null;
17 | }
18 |
19 | unusedFiles = difference(unusedFiles, children); // Remove files from future consideration
20 |
21 | return new Component({
22 | name: dir.name,
23 | path: dir.path,
24 | relative: dir.relative,
25 | root: clone(dir),
26 | scenarios: [],
27 | files: children
28 | });
29 | });
30 |
31 | return compact(components);
32 | };
33 |
--------------------------------------------------------------------------------
/packages/core/src/compile-components.test.js:
--------------------------------------------------------------------------------
1 | const { resolve, join, basename } = require('path');
2 | const { readdirSync, statSync } = require('fs');
3 | const readFiles = require('./read');
4 | const readComponents = require('./compile-components');
5 | const Component = require('./entities/component');
6 |
7 | const componentsPath = resolve(__dirname, '../test/fixtures/components');
8 |
9 | it('Matches all the components in a directory', async () => {
10 | const files = await readFiles(componentsPath);
11 | const components = await readComponents(files);
12 | const componentDirs = recursiveReadDirs(componentsPath).filter(path => basename(path).startsWith('@'));
13 | expect(components.length).toEqual(componentDirs.length);
14 | });
15 |
16 | it('Retuns an array of Component instances', async () => {
17 | const files = await readFiles(componentsPath);
18 | for (const component of await readComponents(files, componentsPath)) {
19 | expect(component).toBeInstanceOf(Component);
20 | }
21 | });
22 |
23 | it('Supports supplying custom matchers', async () => {
24 | const matcher = (dir, children) => {
25 | return dir.name.startsWith('component-') && children.length > 0;
26 | };
27 | const files = await readFiles(resolve(__dirname, '../test/fixtures/custom-matcher'));
28 | const components = await readComponents(files, { matcher });
29 | expect(components.length).toEqual(1);
30 | });
31 |
32 | function recursiveReadDirs(path) {
33 | let list = [];
34 | const files = readdirSync(path);
35 | if (!files.length) {
36 | return list;
37 | }
38 | files.forEach(function(file) {
39 | const filePath = join(path, file);
40 | const stats = statSync(filePath);
41 | if (stats.isDirectory()) {
42 | const children = readdirSync(filePath);
43 | if (children.length) {
44 | list.push(filePath);
45 | list = list.concat(recursiveReadDirs(filePath));
46 | }
47 | }
48 | });
49 | return list;
50 | }
51 |
--------------------------------------------------------------------------------
/packages/core/src/compiler.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require('events');
2 | const { isFunction, debounce } = require('lodash');
3 | const { watch } = require('chokidar');
4 | const { normalizeSrc } = require('@frctl/fractalite-support/utils');
5 | const compose = require('./compose');
6 | const createState = require('./state');
7 | const readFiles = require('./read');
8 | const compileComponents = require('./compile-components');
9 |
10 | module.exports = function(config = {}) {
11 | const emitter = new EventEmitter();
12 | const middlewares = [];
13 | const compiler = {};
14 | const watchCallbacks = [];
15 | const parseSrc = normalizeSrc(config.src || {});
16 | const watchSrc = normalizeSrc(config.watch || {});
17 |
18 | const state = createState();
19 |
20 | Object.defineProperty(compiler, 'src', {
21 | get() {
22 | return { ...parseSrc, watch: { ...watchSrc } };
23 | },
24 | enumerable: true
25 | });
26 |
27 | compiler.use = function(plugin) {
28 | middlewares.push(plugin);
29 | return compiler;
30 | };
31 |
32 | compiler.getState = () => {
33 | return state;
34 | };
35 |
36 | compiler.on = (event, listener) => {
37 | emitter.on(event, listener);
38 | return compiler;
39 | };
40 |
41 | compiler.run = async function() {
42 | emitter.emit('start');
43 | const ctx = {};
44 | const logs = [];
45 | const hrStart = process.hrtime();
46 | const { paths, opts } = parseSrc;
47 |
48 | let files = await readFiles(paths, {
49 | onlyFiles: false,
50 | gitignore: Boolean(opts.gitignore)
51 | });
52 |
53 | ctx.files = files;
54 | ctx.log = (message, level) => {
55 | logs.push({ level, message });
56 | };
57 |
58 | const components = await compileComponents(files, opts);
59 | files = files.filter(f => f.stats.isFile());
60 | const applyPlugins = compose(
61 | middlewares,
62 | ctx
63 | );
64 | await applyPlugins(components);
65 | state.update({ components, files });
66 |
67 | const info = {
68 | logs,
69 | time: process.hrtime(hrStart)
70 | };
71 |
72 | emitter.emit('finish', state, info);
73 | return { state, info };
74 | };
75 |
76 | compiler.watch = function(callback) {
77 | const watchers = [];
78 | const paths = [...parseSrc.paths, ...watchSrc.paths];
79 | const opts = Object.assign({ ignoreInitial: true }, watchSrc.opts);
80 |
81 | const watcher = watch(paths, opts)
82 | .on('all', watchHandler(['add', 'unlink', 'change', 'addDir', 'unlinkDir']))
83 | .on('error', err => watchCallbacks.forEach(cb => cb(err)));
84 |
85 | watchers.push(watcher);
86 |
87 | compiler.watch = function(callback) {
88 | if (isFunction(callback)) {
89 | watchCallbacks.push(callback);
90 | }
91 | };
92 |
93 | compiler.watch(callback);
94 | return watchers;
95 | };
96 |
97 | function watchHandler(parseEvents) {
98 | let lastResult = null;
99 | return debounce(async (event, path) => {
100 | try {
101 | // Only re-parse for 'primary' events, otherwise just notify of changes
102 | lastResult = parseEvents.includes(event) ? await compiler.run() : lastResult;
103 | const info = { path, event, ...lastResult.info };
104 | watchCallbacks.forEach(cb => cb(null, lastResult.state, info));
105 | } catch (err) {
106 | watchCallbacks.forEach(cb => cb(err));
107 | }
108 | }, 300);
109 | }
110 |
111 | return compiler;
112 | };
113 |
--------------------------------------------------------------------------------
/packages/core/src/compiler.test.js:
--------------------------------------------------------------------------------
1 | const createCompiler = require('../.');
2 |
3 | describe('Compiler middleware', () => {
4 | it('Is called when running the compiler', async () => {
5 | let called = false;
6 | const mw = () => {
7 | called = true;
8 | };
9 | const compiler = createCompiler();
10 | compiler.use(mw);
11 | await compiler.run();
12 | expect(called).toBe(true);
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/packages/core/src/component-matcher.js:
--------------------------------------------------------------------------------
1 | module.exports = function(dir, children, opts = {}) {
2 | if (children.length === 0) {
3 | return false; // Components cannot be empty directories
4 | }
5 | const matchFiles = opts.matchFiles || ['package.json', 'view.', 'config.'];
6 | for (const file of children) {
7 | // Check to see if any of the files within the directory
8 | // match any of the key component file names.
9 | for (const matcher of matchFiles) {
10 | if (file.basename.indexOf(matcher) > -1) {
11 | return true;
12 | }
13 | }
14 | }
15 | return false;
16 | };
17 |
--------------------------------------------------------------------------------
/packages/core/src/compose.js:
--------------------------------------------------------------------------------
1 | // Adapted from Koa Compose: https://github.com/koajs/compose
2 |
3 | module.exports = function compose(middleware, ctx = {}) {
4 | if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!');
5 | for (const fn of middleware) {
6 | if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!');
7 | }
8 |
9 | return function(context, next) {
10 | // Last called middleware #
11 | let index = -1;
12 | return dispatch(0);
13 | async function dispatch(i) {
14 | if (i <= index) return Promise.reject(new Error('next() called multiple times'));
15 | index = i;
16 | let fn = middleware[i];
17 | if (i === middleware.length) fn = next;
18 | if (!fn) return Promise.resolve(context);
19 | try {
20 | let called = false;
21 | const trigger = dispatch.bind(null, i + 1);
22 | const wrappedTrigger = () => {
23 | called = true;
24 | return trigger();
25 | };
26 | const rawResult = fn(context, wrappedTrigger, ctx);
27 | const result = Array.isArray(rawResult) ? await Promise.all(rawResult) : await Promise.resolve(rawResult);
28 | if (!called) {
29 | await trigger();
30 | }
31 | return result;
32 | } catch (err) {
33 | return Promise.reject(err);
34 | }
35 | }
36 | };
37 | };
38 |
--------------------------------------------------------------------------------
/packages/core/src/create-adapter.js:
--------------------------------------------------------------------------------
1 | const { isFunction } = require('lodash');
2 | const { map } = require('asyncro');
3 | const { getComponent } = require('./helpers');
4 |
5 | module.exports = function(state, adapter) {
6 | adapter = adapter || (() => '');
7 | const renderComponent = isFunction(adapter) ? adapter : adapter.render;
8 |
9 | if (!isFunction(renderComponent)) {
10 | throw new Error(`Adapter must provide a component render function`);
11 | }
12 |
13 | function render(component, props) {
14 | component = getComponent(state, component, true);
15 | return renderComponent(component, props, { ...state, component });
16 | }
17 |
18 | function renderToStaticMarkup(component, props) {
19 | if (isFunction(adapter.renderToStaticMarkup)) {
20 | return adapter.renderToStaticMarkup(component, props, { ...state, component });
21 | }
22 | return render(component, props);
23 | }
24 |
25 | function renderAll(target, props = []) {
26 | if (Array.isArray(props)) {
27 | return map(props, p => render(target, p));
28 | }
29 | return Promise.all([render(target, props)]);
30 | }
31 |
32 | function renderAllToStaticMarkup(target, props = []) {
33 | if (Array.isArray(props)) {
34 | return map(props, p => renderToStaticMarkup(target, p));
35 | }
36 | return Promise.all([renderToStaticMarkup(target, props)]);
37 | }
38 |
39 | function getTemplateString(component) {
40 | if (isFunction(adapter.getTemplateString)) {
41 | return adapter.getTemplateString(component);
42 | }
43 | return null;
44 | }
45 |
46 | function getPreviewString(content) {
47 | if (isFunction(adapter.getPreviewString)) {
48 | return adapter.getPreviewString(content, state);
49 | }
50 | return content;
51 | }
52 |
53 | return { render, renderAll, renderToStaticMarkup, renderAllToStaticMarkup, getPreviewString, getTemplateString };
54 | };
55 |
--------------------------------------------------------------------------------
/packages/core/src/entities/component.js:
--------------------------------------------------------------------------------
1 | const pupa = require('pupa');
2 | const multimatch = require('multimatch');
3 | const Entity = require('./entity');
4 |
5 | class Component extends Entity {
6 | constructor(props) {
7 | if (!props.name) {
8 | throw new Error(`Components must have a 'name' property defined`);
9 | }
10 | super(props);
11 | }
12 |
13 | get handle() {
14 | return this._handle || this.name;
15 | }
16 |
17 | get treePath() {
18 | return this.relative;
19 | }
20 |
21 | get isComponent() {
22 | return true;
23 | }
24 |
25 | matchFiles(matcher) {
26 | matcher = [].concat(matcher).map(match => pupa(match, this));
27 | return this.files.filter(file => multimatch(file.relative, matcher, { matchBase: true }).length);
28 | }
29 |
30 | static isComponent(item) {
31 | return item instanceof Component;
32 | }
33 | }
34 |
35 | module.exports = Component;
36 |
--------------------------------------------------------------------------------
/packages/core/src/entities/entity.js:
--------------------------------------------------------------------------------
1 | const { assign } = require('@frctl/fractalite-support/utils');
2 |
3 | class Entity {
4 | constructor(props) {
5 | assign(this, props);
6 | }
7 |
8 | set handle(str) {
9 | this._handle = str;
10 | }
11 |
12 | get handle() {
13 | return this._handle;
14 | }
15 |
16 | toString() {
17 | return this.handle;
18 | }
19 |
20 | static isEntity(item) {
21 | return item instanceof Entity;
22 | }
23 | }
24 |
25 | module.exports = Entity;
26 |
--------------------------------------------------------------------------------
/packages/core/src/entities/file.js:
--------------------------------------------------------------------------------
1 | const { readFile, readFileSync } = require('fs-extra');
2 | const Entity = require('./entity');
3 |
4 | class File extends Entity {
5 | constructor(props) {
6 | // TODO: better validation
7 | if (!props.relative) {
8 | throw new Error(`Files must have a '.relative' property defined`);
9 | }
10 | if (!props.path) {
11 | throw new Error(`Files must have a '.path' property defined`);
12 | }
13 | super(props);
14 | }
15 |
16 | get handle() {
17 | return this._handle || this.relative;
18 | }
19 |
20 | get treePath() {
21 | return this.relative;
22 | }
23 |
24 | get size() {
25 | const suffixes = ['B', 'KB', 'MB', 'GB', 'TB'];
26 | const { size } = this.stats;
27 | const i = Math.floor(Math.log(size) / Math.log(1024));
28 | const displaySize = Number((size / 1024 ** i).toFixed(2));
29 | return `${displaySize} ${suffixes[i]}`;
30 | }
31 |
32 | get isFile() {
33 | return true;
34 | }
35 |
36 | setContents(contents) {
37 | this._contents = contents;
38 | }
39 |
40 | getContents() {
41 | return this._contents || readFile(this.path, 'utf-8');
42 | }
43 |
44 | getContentsSync() {
45 | return this._contents || readFileSync(this.path, 'utf-8');
46 | }
47 |
48 | toString() {
49 | return this.relative;
50 | }
51 |
52 | static isFile(item) {
53 | return item instanceof File;
54 | }
55 | }
56 |
57 | module.exports = File;
58 |
--------------------------------------------------------------------------------
/packages/core/src/helpers.js:
--------------------------------------------------------------------------------
1 | const { find } = require('lodash');
2 | const { defaultsDeep } = require('@frctl/fractalite-support/utils');
3 | const Component = require('./entities/component');
4 | const File = require('./entities/file');
5 |
6 | function getComponent({ components }, target, throwOnNotFound = false) {
7 | if (isComponent(target)) return target;
8 | const component = find(components, { name: target });
9 | if (!component && throwOnNotFound) {
10 | throw new Error(`Component '${target}' was not found`);
11 | }
12 | return component;
13 | }
14 |
15 | function getScenario(component, name, throwOnNotFound = false) {
16 | const scenario = find(component.scenarios, { name });
17 | if (!scenario && throwOnNotFound) {
18 | throw new Error(`Scenario '${name}' for component '${component.name}' was not found`);
19 | }
20 | return scenario;
21 | }
22 |
23 | function getScenarioOrDefault(component, name, throwOnNotFound = false) {
24 | if (!name) {
25 | return component.scenarios[0];
26 | }
27 | const scenario = getScenario(component, name, throwOnNotFound);
28 | return scenario ? scenario : component.scenarios[0];
29 | }
30 |
31 | function getFile({ files }, target, throwOnNotFound = false) {
32 | if (isFile(target)) return target;
33 | const file = find(files, { handle: target });
34 | if (!file && throwOnNotFound) {
35 | throw new Error(`File '${target}' was not found`);
36 | }
37 | return file;
38 | }
39 |
40 | function mergeProps(state, ...args) {
41 | // TODO: allow for resolving of string componet/scenario references?
42 | return defaultsDeep(...args.reverse());
43 | }
44 |
45 | function isComponent(component) {
46 | return Component.isComponent(component);
47 | }
48 |
49 | function isFile(file) {
50 | return File.isFile(file);
51 | }
52 |
53 | module.exports = {
54 | getComponent,
55 | getScenario,
56 | getScenarioOrDefault,
57 | getFile,
58 | mergeProps,
59 | isComponent,
60 | isFile
61 | };
62 |
--------------------------------------------------------------------------------
/packages/core/src/html-adapter.js:
--------------------------------------------------------------------------------
1 | const pupa = require('pupa');
2 |
3 | module.exports = function(app, compiler) {
4 | const views = ['view.html', '{name}.view.html'];
5 |
6 | compiler.use(components => {
7 | components.forEach(component => {
8 | const fragments = component.matchFiles(views);
9 | fragments.forEach(file => {
10 | file.isHTMLFragment = true;
11 | });
12 | });
13 | });
14 |
15 | return {
16 | views,
17 | async render(component, props, state) {
18 | const view = component.matchFiles(views)[0];
19 | if (view) {
20 | return pupa(await view.getContents(), props);
21 | }
22 | throw new Error(`Could not render component - no view file found`);
23 | }
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/packages/core/src/middleware/config.js:
--------------------------------------------------------------------------------
1 | const { get, isFunction } = require('lodash');
2 | const deepFreeze = require('deep-freeze');
3 | const rewire = require('enhanced-require');
4 | const pupa = require('pupa');
5 | const { map } = require('asyncro');
6 | const { defaultsDeep } = require('@frctl/fractalite-support/utils');
7 |
8 | module.exports = function(opts = {}) {
9 | opts = defaultsDeep(opts, {
10 | defaults: {},
11 | finder: {
12 | searchPlaces: ['package.json', '{name}.config.js', '{name}.config.json', '{name}.config.yml', 'config.js', 'config.json', 'config.yml'],
13 | packageProp: 'fractal'
14 | },
15 | resolve: {
16 | '~': process.cwd()
17 | },
18 | process: config => config
19 | });
20 |
21 | return async function configMiddleware(components) {
22 | const load = rewire(module, {
23 | recursive: true,
24 | resolve: {
25 | alias: opts.resolve || {}
26 | }
27 | });
28 | const cosmiconfig = load('cosmiconfig');
29 |
30 | await map(components, async component => {
31 | const { path, name } = component.root;
32 | const finderOpts = opts.finder || {};
33 | const cosmiOpts = Object.assign({}, finderOpts, {
34 | stopDir: path,
35 | searchPlaces: get(finderOpts, 'searchPlaces', []).map(path => pupa(path, component.root))
36 | });
37 | const finder = cosmiconfig(name, cosmiOpts);
38 | const searchResult = await finder.search(path);
39 | let config = searchResult ? searchResult.config : {};
40 | if (searchResult) {
41 | component.configFile = component.files.find(file => file.path === searchResult.filepath);
42 | if (isFunction(config)) {
43 | config = await config(components);
44 | }
45 | } else {
46 | // This.debug(`No config file found for component '${component.root.relative}'`);
47 | }
48 | config = defaultsDeep(config, opts.defaults || {});
49 | component.config = deepFreeze(opts.process(config));
50 | return component;
51 | });
52 | };
53 | };
54 |
--------------------------------------------------------------------------------
/packages/core/src/middleware/files.js:
--------------------------------------------------------------------------------
1 | const { relative } = require('path');
2 | const slash = require('slash');
3 |
4 | module.exports = function() {
5 | return function filesMiddleware(components) {
6 | components.forEach(component => {
7 | component.files = component.files.map(file => {
8 | file.componentPath = slash(relative(component.root.path, file.path));
9 | Object.defineProperty(file, 'handle', {
10 | get() {
11 | return `${component.name}/${file.componentPath}`;
12 | },
13 | set(value) {
14 | file._handle = value;
15 | }
16 | });
17 | return file;
18 | });
19 | });
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/packages/core/src/middleware/index.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | key: 'components.opts.files',
4 | handler: require('./files')
5 | },
6 | {
7 | key: 'components.opts.config',
8 | handler: require('./config')
9 | },
10 | {
11 | key: 'components.opts.name',
12 | handler: require('./name')
13 | },
14 | {
15 | key: 'components.opts.label',
16 | handler: require('./label')
17 | },
18 | {
19 | key: 'components.opts.scenarios',
20 | handler: require('./scenarios')
21 | }
22 | ];
23 |
--------------------------------------------------------------------------------
/packages/core/src/middleware/label.js:
--------------------------------------------------------------------------------
1 | const { titlize } = require('@frctl/fractalite-support/utils');
2 |
3 | module.exports = function() {
4 | return function labelMiddlware(components) {
5 | components.forEach(component => {
6 | const config = component.config || {};
7 | component.label = config.label || titlize(config.name || component.name);
8 | });
9 | };
10 | };
11 |
--------------------------------------------------------------------------------
/packages/core/src/middleware/name.js:
--------------------------------------------------------------------------------
1 | const { slugify, normalizeName } = require('@frctl/fractalite-support/utils');
2 |
3 | module.exports = function() {
4 | return function nameMiddleware(components) {
5 | components.forEach(component => {
6 | const config = component.config || {};
7 | component.name = slugify(normalizeName(config.name || component.name));
8 | });
9 | };
10 | };
11 |
--------------------------------------------------------------------------------
/packages/core/src/middleware/scenarios.js:
--------------------------------------------------------------------------------
1 | const { titlize, slugify } = require('@frctl/fractalite-support/utils');
2 | const deepFreeze = require('deep-freeze');
3 |
4 | module.exports = function() {
5 | return function scenariosMiddleware(components) {
6 | components.forEach(component => {
7 | const scenarios = component.config.scenarios || [{ name: 'default' }];
8 | let counter = 0;
9 | component.scenarios = scenarios.map(config => {
10 | counter++;
11 | const name = slugify(config.name || `scenario-${counter}`);
12 | const label = config.label || titlize(name);
13 | const props = config.props || {};
14 |
15 | return {
16 | name,
17 | label,
18 | props,
19 | // Handle: name,
20 | config: deepFreeze(config)
21 | };
22 | });
23 | });
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/packages/core/src/read.js:
--------------------------------------------------------------------------------
1 | const { parse, dirname, relative } = require('path');
2 | const { flatten } = require('lodash');
3 | const globby = require('globby');
4 | const globBase = require('glob-base');
5 | const { stat } = require('fs-extra');
6 | const slash = require('slash');
7 | const { map } = require('asyncro');
8 | const { toArray, normalizeName } = require('@frctl/fractalite-support/utils');
9 | const File = require('./entities/file');
10 |
11 | module.exports = async function(src = [], opts = {}) {
12 | return flatten(
13 | await map(toArray(src), async src => {
14 | const glob = globBase(src);
15 | const root = slash(glob.isGlob ? glob.base : src);
16 | const paths = await globby([src, '!**/node_modules'], {
17 | onlyFiles: opts.onlyFiles || false,
18 | gitignore: opts.gitignore === true
19 | });
20 | return map(paths, path => toFile(path, root));
21 | })
22 | );
23 | };
24 |
25 | async function toFile(path, root) {
26 | const { ext, base, name } = parse(path);
27 | const stats = await stat(path);
28 | path = slash(path);
29 | return new File({
30 | root,
31 | relative: relative(root, path),
32 | path,
33 | basename: base,
34 | dirname: dirname(path),
35 | extname: ext,
36 | ext: ext.toLowerCase(),
37 | stats,
38 | name: normalizeName(name)
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/packages/core/src/read.test.js:
--------------------------------------------------------------------------------
1 | const { resolve, join, basename } = require('path');
2 | const { readdirSync, statSync } = require('fs');
3 | const readFiles = require('./read');
4 | const File = require('./entities/file');
5 |
6 | const componentsPath = resolve(__dirname, '../test/fixtures/components');
7 | const withIgnoredPath = resolve(__dirname, '../test/fixtures/ignored');
8 |
9 | it('Recursively reads all the files in a directory', async () => {
10 | const files = await readFiles(componentsPath);
11 | const paths = readRecursive(componentsPath);
12 | expect(files.length).toEqual(paths.length);
13 | });
14 |
15 | it('Ignores node_modules directories', async () => {
16 | const files = await readFiles(withIgnoredPath);
17 | expect(files.filter(file => file.relative.includes('node_modules')).length).toEqual(0);
18 | });
19 |
20 | it('Retuns an array of File instances', async () => {
21 | for (const file of await readFiles(componentsPath)) {
22 | expect(file).toBeInstanceOf(File);
23 | }
24 | });
25 |
26 | function readRecursive(path, opts = {}) {
27 | let list = [];
28 | const files = readdirSync(path);
29 | if (!files.length) {
30 | return list;
31 | }
32 | files.forEach(function(file) {
33 | const filePath = join(path, file);
34 | const stats = statSync(filePath);
35 | if (basename(filePath) !== 'node_modules') {
36 | if (stats.isDirectory()) {
37 | list.push(filePath);
38 | list = list.concat(readRecursive(filePath));
39 | } else {
40 | list.push(filePath);
41 | }
42 | }
43 | });
44 | return list;
45 | }
46 |
--------------------------------------------------------------------------------
/packages/core/src/state.js:
--------------------------------------------------------------------------------
1 | module.exports = function(initial = {}) {
2 | const state = {
3 | components: [],
4 | files: []
5 | };
6 |
7 | state.update = props => {
8 | Object.assign(state, props);
9 | return state;
10 | };
11 |
12 | state.update(initial);
13 |
14 | return state;
15 | };
16 |
--------------------------------------------------------------------------------
/packages/core/src/state.test.js:
--------------------------------------------------------------------------------
1 | const createState = require('./state');
2 |
3 | test('exports a function', () => {
4 | expect(createState).toBeInstanceOf(Function);
5 | });
6 |
7 | test('sets initial state from initialiser object', () => {
8 | const components = [{ name: 'foo' }];
9 | const state = createState({ components });
10 | expect(state.components).toBe(components);
11 | });
12 |
13 | describe('state', () => {
14 | test('has components property', () => {
15 | const state = createState();
16 | expect(state).toHaveProperty('components');
17 | expect(state.components).toBeInstanceOf(Array);
18 | });
19 |
20 | test('has files property', () => {
21 | const state = createState();
22 | expect(state).toHaveProperty('files');
23 | expect(state.files).toBeInstanceOf(Array);
24 | });
25 |
26 | test('can be updated', () => {
27 | const state = createState();
28 | expect(state.components).toHaveLength(0);
29 | const components = [{ name: 'foo' }];
30 | state.update({ components });
31 | expect(state.components).toBe(components);
32 | });
33 |
34 | test('can have additional properties set', () => {
35 | const state = createState();
36 | const pages = [{ name: 'foo' }];
37 | state.update({ pages });
38 | expect(state).toHaveProperty('pages');
39 | expect(state.pages).toBe(pages);
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/packages/core/test/.gitignore:
--------------------------------------------------------------------------------
1 | !node_modules/
2 |
--------------------------------------------------------------------------------
/packages/core/test/fixtures/components/@standard/standard.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | label: 'Standard Component'
3 | };
4 |
--------------------------------------------------------------------------------
/packages/core/test/fixtures/components/@standard/view.html:
--------------------------------------------------------------------------------
1 | A component
2 |
--------------------------------------------------------------------------------
/packages/core/test/fixtures/components/nested/@yaml/view.html:
--------------------------------------------------------------------------------
1 | A component
2 |
--------------------------------------------------------------------------------
/packages/core/test/fixtures/components/nested/@yaml/yaml.config.yml:
--------------------------------------------------------------------------------
1 | label: 'Component with YAML format configuration'
2 |
--------------------------------------------------------------------------------
/packages/core/test/fixtures/custom-matcher/component-standard/view.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frctl/fractalite/8e1dd708265df58921e9061d787e59dd3022d1d6/packages/core/test/fixtures/custom-matcher/component-standard/view.html
--------------------------------------------------------------------------------
/packages/core/test/fixtures/ignored/@with-node-modules/node_modules/ignored.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frctl/fractalite/8e1dd708265df58921e9061d787e59dd3022d1d6/packages/core/test/fixtures/ignored/@with-node-modules/node_modules/ignored.js
--------------------------------------------------------------------------------
/packages/core/test/fixtures/ignored/@with-node-modules/view.html:
--------------------------------------------------------------------------------
1 | view
2 |
--------------------------------------------------------------------------------
/packages/core/test/integration.test.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 | const createCompiler = require('../.');
3 | const standardConfig = require('./fixtures/components/@standard/standard.config');
4 |
5 | it('Reads JS config files', async () => {
6 | const compiler = createCompiler(resolve(__dirname, './fixtures/components'));
7 | const { state } = await compiler.run();
8 | const standard = state.components.find(c => c.name === 'standard');
9 | expect(standard.config).toEqual(standardConfig);
10 | });
11 |
--------------------------------------------------------------------------------
/packages/fractalite/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "targets": {
5 | "browsers": ["last 2 Chrome versions"]
6 | }
7 | }]
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/fractalite/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/packages/fractalite/README.md:
--------------------------------------------------------------------------------
1 | ## @frctl/fractalite
2 |
3 | A UI for component-driven development.
4 |
5 | ```
6 | npm i @frctl/fractalite
7 | ```
8 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/app.js:
--------------------------------------------------------------------------------
1 | import './app.scss';
2 | import '../src/client/app.js';
3 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/app.scss:
--------------------------------------------------------------------------------
1 | @import 'normalize.css/normalize.css';
2 |
3 | @import './css/theme';
4 | @import './css/base';
5 | @import './css/app';
6 | @import './css/prose';
7 | @import './css/brand';
8 | @import './css/nav';
9 | @import './css/search';
10 | @import './css/inspector';
11 | @import './css/preview';
12 | @import './css/page';
13 | @import './css/controls';
14 | @import './css/tabs';
15 | @import './css/panels';
16 | @import './css/split';
17 | @import './css/error';
18 | @import './css/highlight';
19 | @import './css/json-explorer';
20 | @import './css/source-code';
21 | @import './css/proptable';
22 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/app.scss:
--------------------------------------------------------------------------------
1 | .fr-app {
2 | height: 100vh;
3 | width: 100vw;
4 | padding: 0;
5 |
6 | &__container {
7 | background-color: var(--bg-color-light);
8 | display: flex;
9 | width: 100vw;
10 | height: 100vh;
11 | overflow: hidden;
12 | display: none;
13 | &.is-ready {
14 | display: flex;
15 | }
16 | }
17 |
18 | &__sidebar {
19 | width: var(--sidebar-width);
20 | background-color: var(--sidebar-bg-color);
21 | color: var(--sidebar-text-color);
22 | height: 100%;
23 | display: flex;
24 | flex-direction: column;
25 | a {
26 | color: var(--sidebar-link-color);
27 | }
28 | }
29 |
30 | &__brand {
31 | flex: none;
32 | }
33 |
34 | &__sidebar-main {
35 | flex: 1 1 50%;
36 | overflow: auto;
37 | -webkit-overflow-scrolling: touch;
38 | }
39 |
40 | &__navigation {
41 | padding-top: 10px;
42 | padding-bottom: 10px;
43 | }
44 |
45 | &__search {
46 | flex: 0 0 auto;
47 | }
48 |
49 | &__controls {
50 | flex: none;
51 | }
52 |
53 | &__main {
54 | width: calc(100% - var(--sidebar-width));
55 | height: 100vh;
56 | position: relative;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/base.scss:
--------------------------------------------------------------------------------
1 | *,
2 | *:before,
3 | *:after {
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: var(--font-family);
9 | width: 100vw;
10 | height: 100vh;
11 | color: var(--text-color);
12 | line-height: 1.4;
13 | font-size: var(--font-size);
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | }
17 |
18 | body {
19 | width: 100vw;
20 | height: 100vh;
21 | }
22 |
23 | a {
24 | color: var(--link-color);
25 | text-decoration: var(--link-decoration);
26 | }
27 |
28 | h1,
29 | h2,
30 | h3,
31 | h4,
32 | h5,
33 | h6 {
34 | margin: 0;
35 | }
36 |
37 | ul,
38 | ol {
39 | padding: 0;
40 | margin: 0;
41 | list-style: none;
42 | }
43 |
44 | pre {
45 | margin: 0;
46 | }
47 |
48 | pre,
49 | code {
50 | font-family: var(--font-family);
51 | }
52 |
53 | [data-mode='build'].loading,
54 | [data-mode='build'] .loading {
55 | background-color: var(--bg-color-light);
56 | background-image: var(--bg-icon-loader);
57 | background-position: center;
58 | background-repeat: no-repeat;
59 | }
60 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/brand.scss:
--------------------------------------------------------------------------------
1 | .fr-brand {
2 | padding: 16px 16px;
3 | font-size: 20px;
4 |
5 | a {
6 | color: #fff;
7 | font-weight: bold;
8 | text-decoration: none;
9 | }
10 |
11 | &__brand {
12 | font-weight: bold;
13 | font-size: 20px;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/controls.scss:
--------------------------------------------------------------------------------
1 | .fr-controls {
2 | flex: none;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/error.scss:
--------------------------------------------------------------------------------
1 | .fr-error {
2 | padding: 20px;
3 | background: var(--error-bg-color);
4 | color: var(--error-text-color);
5 | overflow: auto;
6 | height: 100%;
7 | width: 100%;
8 | display: flex;
9 | flex-direction: column;
10 | justify-content: center;
11 | align-items: center;
12 | text-align: center;
13 |
14 | &__title {
15 | font-size: var(--font-size--xlarge);
16 | }
17 |
18 | &__message {
19 | opacity: 0.9;
20 | font-size: 0.9em;
21 | }
22 |
23 | &__stack {
24 | opacity: 0.8;
25 | font-family: var(--font-family--code);
26 | font-size: var(--font-size--code);
27 | white-space: pre-wrap;
28 | overflow: hidden;
29 | word-break: break-all;
30 | }
31 |
32 | &__content {
33 | max-width: 600px;
34 | }
35 |
36 | &--overlay {
37 | display: none;
38 | position: absolute;
39 | top: 0;
40 | bottom: 0;
41 | left: 0;
42 | right: 0;
43 | width: 100%;
44 | height: 100%;
45 | background: var(--error-bg-color--overlay);
46 | z-index: 10000;
47 | justify-content: start;
48 | align-items: start;
49 | text-align: left;
50 | }
51 |
52 | &--overlay.is-active {
53 | display: block;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/highlight.scss:
--------------------------------------------------------------------------------
1 | .hljs {
2 | font-family: var(--font-family--code);
3 | font-size: var(--font-size--code);
4 | background: var(--bg-color-light-shade);
5 | color: var(--text-color);
6 | display: block;
7 | overflow-x: auto;
8 | padding: 0.5em;
9 |
10 | &-comment,
11 | &-quote {
12 | color: #998;
13 | font-style: italic;
14 | }
15 |
16 | &-keyword,
17 | &-selector-tag,
18 | &-subst {
19 | color: #333;
20 | font-weight: bold;
21 | }
22 |
23 | &-number,
24 | &-literal,
25 | &-variable,
26 | &-template-variable,
27 | &-tag &-attr {
28 | color: #008080;
29 | }
30 |
31 | &-string,
32 | &-doctag {
33 | color: #d14;
34 | }
35 |
36 | &-title,
37 | &-section,
38 | &-selector-id {
39 | color: #900;
40 | font-weight: bold;
41 | }
42 |
43 | &-subst {
44 | font-weight: normal;
45 | }
46 |
47 | &-type,
48 | &-class &-title {
49 | color: #458;
50 | font-weight: bold;
51 | }
52 |
53 | &-tag,
54 | &-name,
55 | &-attribute {
56 | color: #000080;
57 | font-weight: normal;
58 | }
59 |
60 | &-regexp,
61 | &-link {
62 | color: #009926;
63 | }
64 |
65 | &-symbol,
66 | &-bullet {
67 | color: #990073;
68 | }
69 |
70 | &-built_in,
71 | &-builtin-name {
72 | color: #0086b3;
73 | }
74 |
75 | &-meta {
76 | color: #999;
77 | font-weight: bold;
78 | }
79 |
80 | &-deletion {
81 | background: #fdd;
82 | }
83 |
84 | &-addition {
85 | background: #dfd;
86 | }
87 |
88 | &-emphasis {
89 | font-style: italic;
90 | }
91 |
92 | &-strong {
93 | font-weight: bold;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/inspector.scss:
--------------------------------------------------------------------------------
1 | .fr-inspector {
2 | width: 100%;
3 | height: 100vh;
4 | background: var(--bg-color-light-shade);
5 | overflow: hidden;
6 |
7 | &__preview {
8 | background-color: #fff;
9 | position: relative;
10 | }
11 |
12 | &__drawer {
13 | flex: 1;
14 | background-color: #fff;
15 | overflow: hidden;
16 | display: flex;
17 | flex-direction: column;
18 | }
19 |
20 | &__tabs {
21 | flex: none;
22 | }
23 |
24 | &__panels {
25 | flex: 1;
26 | }
27 |
28 | .gutter.gutter-vertical {
29 | border-top: 1px solid var(--divider-color);
30 | background-color: var(--tabs-bg-color);
31 | background-image: none;
32 | }
33 |
34 | .fr-source-code,
35 | .fr-json-explorer {
36 | height: 100%;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/json-explorer.scss:
--------------------------------------------------------------------------------
1 | .fr-json-explorer {
2 | padding: 12px;
3 | background-color: var(--bg-color-light);
4 |
5 | .vjs__tree {
6 | line-height: 1.3;
7 | font-family: var(--font-family--code);
8 | font-size: var(--font-size--code);
9 | color: var(--text-color);
10 |
11 | .vjs__tree__content {
12 | border-left: 1px dotted var(--divider-color);
13 | }
14 |
15 | .vjs__tree__node {
16 | &:hover {
17 | color: #20a0ff;
18 | }
19 | }
20 |
21 | .vjs__value__null {
22 | color: #ff4949;
23 | }
24 |
25 | .vjs__value__number,
26 | .vjs__value__boolean {
27 | color: #1d8ce0;
28 | }
29 |
30 | .vjs__value__string {
31 | color: rgb(170, 17, 17);
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/nav.scss:
--------------------------------------------------------------------------------
1 | .fr-nav {
2 | &__items {
3 | list-style: none;
4 | margin: 0;
5 | padding: 0;
6 | font-size: var(--font-size--small);
7 | }
8 |
9 | &__item > &__items {
10 | display: none;
11 | }
12 |
13 | &__item.is-expanded > &__items {
14 | display: block;
15 | }
16 |
17 | &__link,
18 | &__toggle {
19 | user-select: none;
20 | display: block;
21 | padding: 6px 16px;
22 | text-decoration: none;
23 | transition: all 0.15s ease-out;
24 | position: relative;
25 | }
26 |
27 | &__toggle {
28 | color: var(--nav-text-color);
29 | position: relative;
30 | opacity: 0.7;
31 | &.is-collapsable:before {
32 | display: inline-block;
33 | content: '›';
34 | font-weight: bold;
35 | position: relative;
36 | margin-left: -10px;
37 | opacity: 0.5;
38 | margin-right: 5px;
39 | }
40 | }
41 |
42 | &__item.is-expanded > &__toggle:before {
43 | transform: rotate(90deg);
44 | }
45 |
46 | &__toggle:hover {
47 | cursor: pointer;
48 | &:before {
49 | opacity: 1;
50 | }
51 | }
52 |
53 | &__link {
54 | // font-weight: bold;
55 | color: var(--nav-link-color);
56 | }
57 |
58 | &__link:hover {
59 | background-color: var(--nav-link-bg--hover);
60 | color: var(--nav-link-color--hover);
61 | }
62 |
63 | &__link:not(.fr-nav__toggle).is-active {
64 | background-color: var(--nav-link-bg--active);
65 | color: var(--nav-link-color--active);
66 | }
67 |
68 | @for $i from 1 through 6 {
69 | [data-level='#{$i}'] &__toggle,
70 | [data-level='#{$i}'] &__link {
71 | padding-left: 16px * $i;
72 | }
73 | }
74 |
75 | [data-level='1'] > li > &__toggle {
76 | // font-weight: bold;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/page.scss:
--------------------------------------------------------------------------------
1 | .fr-page {
2 | width: 100%;
3 | height: 100%;
4 | font-family: var(--font-family);
5 | background-color: var(--page-bg-color);
6 | overflow-y: scroll;
7 | -webkit-overflow-scrolling: touch;
8 |
9 | &__container {
10 | padding: 16px 24px;
11 | }
12 |
13 | &__title {
14 | font-size: var(--font-size--xlarge);
15 | border-bottom: 1px solid var(--divider-color);
16 | padding-bottom: 12px;
17 | margin-bottom: 24px;
18 | }
19 |
20 | &__content {
21 | max-width: 900px;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/panels.scss:
--------------------------------------------------------------------------------
1 | .fr-panels {
2 | background-color: var(--panel-bg-color);
3 | overflow: hidden;
4 |
5 | &__panel {
6 | overflow: auto;
7 | -webkit-overflow-scrolling: touch;
8 | height: 100%;
9 | width: 100%;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/preview.scss:
--------------------------------------------------------------------------------
1 | .fr-preview {
2 | width: 100%;
3 | height: 100%;
4 | display: flex;
5 | background-color: var(--preview-bg-color);
6 |
7 | &__pane {
8 | width: 100%;
9 | height: 100%;
10 | }
11 |
12 | &__wrapper {
13 | height: 100%;
14 | width: 100%;
15 | position: relative;
16 | }
17 |
18 | &__wrapper.loading &__window {
19 | opacity: 0;
20 | }
21 |
22 | &__stats {
23 | background: var(--bg-color-light-shade);
24 | position: absolute;
25 | right: 0;
26 | bottom: 0;
27 | display: inline-block;
28 | font-family: var(--font-family--code);
29 | font-size: 14px;
30 | padding: 3px 4px;
31 | opacity: 0;
32 | pointer-events: none;
33 | transition: opacity 0.2s ease-out;
34 | }
35 |
36 | &__stats.visible {
37 | opacity: 0.9;
38 | }
39 |
40 | &__spacer {
41 | background: var(--bg-color-light-shade);
42 | }
43 |
44 | &__window {
45 | width: 100%;
46 | height: 100%;
47 | border: 0;
48 | position: absolute;
49 | top: 0;
50 | left: 0;
51 | right: 0;
52 | bottom: 0;
53 | }
54 |
55 | .gutter.gutter-horizontal {
56 | background-color: var(--preview-resizer-bg-color);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/proptable.scss:
--------------------------------------------------------------------------------
1 | .fr-proptable {
2 | font-family: var(--font-family);
3 | background-color: var(--bg-color-light);
4 | display: table;
5 | font-size: 15px;
6 | border-collapse: collapse;
7 |
8 | &__row {
9 | display: table-row;
10 | }
11 |
12 | &__cell {
13 | display: table-cell;
14 | border: 1px solid var(--divider-color);
15 | padding: 3px 5px;
16 | }
17 |
18 | &__cell--fit {
19 | width: 1%;
20 | white-space: nowrap;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/prose.scss:
--------------------------------------------------------------------------------
1 | .fr-prose {
2 | font-family: var(--font-family);
3 |
4 | > h2 {
5 | font-size: 22px;
6 | margin-bottom: 1em;
7 | margin-top: 1em;
8 | }
9 |
10 | > h3 {
11 | font-size: 20px;
12 | margin-bottom: 1em;
13 | margin-top: 1em;
14 | }
15 |
16 | > h4 {
17 | font-size: 18px;
18 | margin-bottom: 1em;
19 | margin-top: 1em;
20 | }
21 |
22 | ul,
23 | ol {
24 | list-style: disc;
25 | margin-left: 20px;
26 | }
27 |
28 | li {
29 | margin-bottom: 0.2em;
30 | }
31 |
32 | :first-child {
33 | margin-top: 0;
34 | }
35 |
36 | :last-child {
37 | margin-bottom: 0;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/search.scss:
--------------------------------------------------------------------------------
1 | .fr-search {
2 | padding: 0 0;
3 | background-color: var(--search-bg-color);
4 |
5 | &__input-wrapper {
6 | position: relative;
7 | }
8 |
9 | &__input-wrapper:hover &__cancel {
10 | opacity: 1;
11 | }
12 |
13 | &__cancel {
14 | position: absolute;
15 | top: 50%;
16 | transform: translateY(-50%);
17 | right: 4px;
18 | color: #fff;
19 | z-index: 10;
20 | display: inline-block;
21 | padding: 5px 10px;
22 | line-height: 1;
23 | cursor: pointer;
24 | opacity: 0.7;
25 | }
26 |
27 | &__input {
28 | width: calc(100% - 32px);
29 | width: 100%;
30 | border: 0;
31 | background-color: transparent;
32 | padding: 12px 40px 12px 16px;
33 | color: #fff;
34 | font-size: 14px;
35 | &:focus {
36 | outline: none;
37 | }
38 | }
39 |
40 | &__results {
41 | padding-bottom: 10px;
42 | font-size: var(--font-size--small);
43 | }
44 |
45 | &__none {
46 | padding: 6px 16px 0 16px;
47 | font-style: italic;
48 | opacity: 0.5;
49 | }
50 |
51 | &__label,
52 | &__link {
53 | user-select: none;
54 | display: block;
55 | padding: 6px 16px;
56 | text-decoration: none;
57 | transition: all 0.15s ease-out;
58 | position: relative;
59 | .highlight {
60 | font-weight: bold;
61 | }
62 | }
63 |
64 | &__label {
65 | opacity: 0.7;
66 | color: var(--nav-text-color);
67 | position: relative;
68 | }
69 |
70 | &__link {
71 | // font-weight: bold;
72 | color: var(--nav-link-color);
73 | padding-left: 32px;
74 | }
75 |
76 | &__link:hover {
77 | background-color: var(--nav-link-bg--hover);
78 | color: var(--nav-link-color--hover);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/source-code.scss:
--------------------------------------------------------------------------------
1 | @import 'codemirror/lib/codemirror.css';
2 |
3 | .fr-source-code {
4 | font-family: var(--font-family--code) !important;
5 | font-size: var(--font-size--code);
6 | background-color: var(--bg-color-light);
7 |
8 | .vue-codemirror {
9 | height: 100%;
10 | }
11 |
12 | .CodeMirror {
13 | height: 100%;
14 | }
15 |
16 | .CodeMirror-scroll {
17 | margin-bottom: 0 !important;
18 | }
19 |
20 | .CodeMirror-gutters {
21 | border-right: 1px solid var(--divider-color);
22 | background-color: var(--bg-color-light-shade);
23 | }
24 |
25 | // TODO: extract colours to CSS variables
26 |
27 | .cm-s-default .cm-header {
28 | color: blue;
29 | }
30 | .cm-s-default .cm-quote {
31 | color: #090;
32 | }
33 | .cm-negative {
34 | color: #d44;
35 | }
36 | .cm-positive {
37 | color: #292;
38 | }
39 | .cm-header,
40 | .cm-strong {
41 | font-weight: bold;
42 | }
43 | .cm-em {
44 | font-style: italic;
45 | }
46 | .cm-link {
47 | text-decoration: underline;
48 | }
49 | .cm-strikethrough {
50 | text-decoration: line-through;
51 | }
52 |
53 | .cm-s-default .cm-keyword {
54 | color: #708;
55 | }
56 | .cm-s-default .cm-atom {
57 | color: #219;
58 | }
59 | .cm-s-default .cm-number {
60 | color: #164;
61 | }
62 | .cm-s-default .cm-def {
63 | color: #00f;
64 | }
65 | .cm-s-default .cm-variable,
66 | .cm-s-default .cm-punctuation,
67 | .cm-s-default .cm-property,
68 | .cm-s-default .cm-operator {
69 | }
70 | .cm-s-default .cm-variable-2 {
71 | color: #05a;
72 | }
73 | .cm-s-default .cm-variable-3,
74 | .cm-s-default .cm-type {
75 | color: #085;
76 | }
77 | .cm-s-default .cm-comment {
78 | color: #a50;
79 | }
80 | .cm-s-default .cm-string {
81 | color: #a11;
82 | }
83 | .cm-s-default .cm-string-2 {
84 | color: #f50;
85 | }
86 | .cm-s-default .cm-meta {
87 | color: #555;
88 | }
89 | .cm-s-default .cm-qualifier {
90 | color: #555;
91 | }
92 | .cm-s-default .cm-builtin {
93 | color: #30a;
94 | }
95 | .cm-s-default .cm-bracket {
96 | color: #997;
97 | }
98 | .cm-s-default .cm-tag {
99 | color: #170;
100 | }
101 | .cm-s-default .cm-attribute {
102 | color: #00c;
103 | }
104 | .cm-s-default .cm-hr {
105 | color: #999;
106 | }
107 | .cm-s-default .cm-link {
108 | color: #00c;
109 | }
110 |
111 | .cm-s-default .cm-error {
112 | color: #f00;
113 | }
114 | .cm-invalidchar {
115 | color: #f00;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/split.scss:
--------------------------------------------------------------------------------
1 | .split-container {
2 | height: 100%;
3 |
4 | & > * {
5 | position: relative;
6 | overflow-y: hidden;
7 | }
8 |
9 | &--horizontal {
10 | display: flex;
11 | }
12 |
13 | .gutter {
14 | background-color: #eee;
15 | background-repeat: no-repeat;
16 | background-position: 50%;
17 | }
18 |
19 | .gutter.gutter-vertical {
20 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
21 | cursor: ns-resize;
22 | }
23 |
24 | .gutter.gutter-horizontal {
25 | float: left;
26 | cursor: ew-resize;
27 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
28 | }
29 | }
30 |
31 | .split {
32 | overflow-y: auto;
33 | overflow-x: hidden;
34 | }
35 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/tabs.scss:
--------------------------------------------------------------------------------
1 | .fr-tabs {
2 | background-color: var(--tabs-bg-color);
3 | display: flex;
4 | width: 100%;
5 | border-bottom: 1px solid var(--divider-color);
6 | align-items: stretch;
7 | user-select: none;
8 |
9 | &__tab {
10 | flex: 1;
11 | }
12 |
13 | &__tab a {
14 | color: var(--text-color);
15 | display: block;
16 | font-size: 14px;
17 | padding: 2px 0 10px 0;
18 | text-align: center;
19 | text-decoration: none;
20 | margin-bottom: -1px;
21 | position: relative;
22 | }
23 |
24 | &__tab:hover a {
25 | border-bottom: 2px solid var(--tabs-highlight-color--hover);
26 | }
27 |
28 | &__tab.is-current a {
29 | font-weight: bold;
30 | border-bottom: 2px solid var(--tabs-highlight-color--active);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/css/theme.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --font-size: 16px;
3 | --font-size--small: 14px;
4 | --font-size--large: 20px;
5 | --font-size--xlarge: 24px;
6 |
7 | --font-size--code: var(--font-size--small);
8 |
9 | --font-family: 'system-ui', 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
10 | --font-family--code: monospace;
11 |
12 | --text-color: #333;
13 | --link-color: #0074d9;
14 | --link-decoration: underline;
15 |
16 | --bg-color-light: #fff;
17 | --bg-color-light-shade: #f8f8f8;
18 | --bg-color-dark: #333;
19 | --bg-icon-loader: url("data:image/svg+xml,%3Csvg width='44' height='44' xmlns='http://www.w3.org/2000/svg' class='loader__icon'%3E%3Cg fill='none' stroke='%23333' fill-rule='evenodd' stroke-width='2'%3E%3Ccircle cx='22' cy='22' r='1'%3E%3Canimate attributeName='r' begin='0s' dur='1.8s' values='1; 20' calcMode='spline' keyTimes='0; 1' keySplines='0.165, 0.84, 0.44, 1' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-opacity' begin='0s' dur='1.8s' values='1; 0' calcMode='spline' keyTimes='0; 1' keySplines='0.3, 0.61, 0.355, 1' repeatCount='indefinite'/%3E%3C/circle%3E%3Ccircle cx='22' cy='22' r='1'%3E%3Canimate attributeName='r' begin='-0.9s' dur='1.8s' values='1; 20' calcMode='spline' keyTimes='0; 1' keySplines='0.165, 0.84, 0.44, 1' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-opacity' begin='-0.9s' dur='1.8s' values='1; 0' calcMode='spline' keyTimes='0; 1' keySplines='0.3, 0.61, 0.355, 1' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
20 |
21 | --divider-color: #ddd;
22 |
23 | --sidebar-width: 260px;
24 | --sidebar-bg-color: var(--bg-color-dark);
25 | --sidebar-text-color: #fff;
26 | --sidebar-link-color: rgba(#fff, 0.7);
27 |
28 | --nav-text-color: var(--sidebar-text-color);
29 | --nav-link-color: var(--sidebar-link-color);
30 | --nav-link-color--hover: var(--nav-link-color);
31 | --nav-link-bg--hover: #444;
32 | --nav-link-color--active: var(--sidebar-link-color);
33 | --nav-link-bg--active: #555;
34 |
35 | --search-bg-color: rgba(0, 0, 0, 0.5);
36 |
37 | --page-bg-color: var(--bg-color-light);
38 | --panel-bg-color: var(--bg-color-light);
39 |
40 | --error-bg-color: rgba(154, 0, 0, 1);
41 | --error-bg-color--overlay: rgba(154, 0, 0, 0.95);
42 | --error-text-color: #fff;
43 |
44 | --tabs-bg-color: var(--bg-color-light-shade);
45 | --tabs-highlight-color--active: #333;
46 | --tabs-highlight-color--hover: #ccc;
47 |
48 | --preview-bg-color: var(--bg-color-light);
49 | --preview-resizer-bg-color: #eee;
50 | }
51 |
--------------------------------------------------------------------------------
/packages/fractalite/assets/reload.js:
--------------------------------------------------------------------------------
1 | import io from 'socket.io-client';
2 |
3 | function debounce(func, wait, immediate) {
4 | var timeout;
5 | return function() {
6 | var context = this,
7 | args = arguments;
8 | var later = function() {
9 | timeout = null;
10 | if (!immediate) func.apply(context, args);
11 | };
12 | var callNow = immediate && !timeout;
13 | clearTimeout(timeout);
14 | timeout = setTimeout(later, wait);
15 | if (callNow) func.apply(context, args);
16 | };
17 | }
18 |
19 | const socket = io();
20 | const reload = debounce(state => window.location.reload(), 500, true);
21 | socket.on('state.updated', reload);
22 |
--------------------------------------------------------------------------------
/packages/fractalite/bin/fractalite:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node --no-warnings
2 |
3 | const { relative, resolve, dirname } = require('path');
4 | const mri = require('mri');
5 | const { cyan, dim, red } = require('kleur');
6 | const { Signale } = require('signale');
7 | const { stripIndent, source } = require('common-tags');
8 | const { loadConfig } = require('@frctl/fractalite-support');
9 | const { serveStatic } = require('@frctl/fractalite-app');
10 | const ip = require('ip');
11 | const fractalite = require('../.');
12 | const { assign } = Object;
13 |
14 | const logger = new Signale({
15 | types: {
16 | debug: {
17 | badge: '✎',
18 | color: 'gray'
19 | }
20 | }
21 | });
22 |
23 | const cwd = process.cwd();
24 | const args = mri(process.argv.slice(2));
25 |
26 | (async () => {
27 | let loaded;
28 |
29 | try {
30 | loaded = await loadConfig({ cwd, path: args.config, name: 'fractal' });
31 | const rootPath = dirname(loaded.path);
32 | if (cwd !== rootPath) {
33 | process.chdir(rootPath);
34 | }
35 | logger.info(`Using config file ${cyan(`./${relative(cwd, loaded.path)}`)}`);
36 | } catch(err) {
37 | if (err.type === 'CONFIG_NOT_FOUND') {
38 | // TODO: run interactive config creator?
39 | loaded = { config: {} };
40 | logger.warn(`Config file not found. Continuing without config.`);
41 | } else {
42 | logger.error(err);
43 | process.exit(1);
44 | }
45 | }
46 |
47 | try {
48 | const command = args._[0] || 'start';
49 | const { config } = loaded;
50 |
51 | if (command === 'start') {
52 |
53 | const mode = assign({ mode: 'develop' }, config.develop || {}, args);
54 | const app = fractalite({ mode, ...config });
55 |
56 | app.on('error', function onError(err) {
57 | if (err.message.indexOf('ECONNRESET') >= 0) {
58 | return;
59 | }
60 | if (err.status === 404) {
61 | logger.warn(`${err.message} ${dim(`[${err.path}]`)}`);
62 | } else {
63 | logger.error(err);
64 | }
65 | });
66 |
67 | app.on('state.pending', () => logger.info(`Source change detected`));
68 | app.on('state.updated', (state, info) => {
69 | for (const log of info.logs || []) {
70 | log.message instanceof Error ? logger.error(log.message) : logger[log.level || 'info'](log.message);
71 | }
72 | logger.success(`Compiled in %d.%ds`, info.time[0], Math.round(info.time[1] / 1000000))
73 | logger.log('')
74 | });
75 |
76 | const server = await app.run();
77 | const port = server.address().port;
78 |
79 | serverStarted({
80 | server,
81 | startedMessage: `Fractalite server started`,
82 | stoppedMessage: `Fractalite server stopped`
83 | });
84 |
85 | } else if (command === 'build') {
86 |
87 | const mode = assign({ mode: 'build' }, config.build || {}, args);
88 | const app = fractalite({ mode, ...config });
89 |
90 | const interactiveLogger = new Signale({interactive: true});
91 |
92 | app.on('build.start', () => {
93 | interactiveLogger.await(`Building static export...`);
94 | });
95 |
96 | app.on('build.end', results => {
97 | interactiveLogger.success(`Fractalite static build complete. ${results.length} files generated.`);
98 | });
99 |
100 | await app.run();
101 |
102 | if (mode.serve) {
103 | const server = await serveStatic(mode.dest, mode.port);
104 | serverStarted({
105 | server,
106 | startedMessage: `Serving static build`,
107 | stoppedMessage: `Fractalite static build server stopped`
108 | });
109 | }
110 |
111 | } else if (command === 'serve') {
112 |
113 | const opts = assign(config.build || {}, args);
114 | const server = await serveStatic(opts.dest, opts.port);
115 |
116 | serverStarted({ server });
117 |
118 | } else {
119 | throw new Error(`Command '${command}' not recognised`);
120 | }
121 |
122 | } catch(err) {
123 | logger.error(err);
124 | process.exit(1);
125 | }
126 |
127 | function serverStarted({ server, startedMessage, stoppedMessage }){
128 | const port = server.address().port;
129 |
130 | logger.success(startedMessage || 'Server started');
131 | logger.log('');
132 | logger.log(stripIndent`
133 | ---
134 | Local: ${cyan(`http://localhost:${port}`)}
135 | Network: ${cyan(`http://${ip.address()}:${port}`)}
136 | ---
137 | ${dim(`Use ^C to stop`)}
138 | `);
139 | logger.log('');
140 |
141 | process.on('SIGINT', () => {
142 | server.close();
143 | logger.log('');
144 | logger.info(stoppedMessage || 'Server stopped');
145 | process.exit(0);
146 | });
147 | }
148 |
149 | })();
150 |
--------------------------------------------------------------------------------
/packages/fractalite/index.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 | const { forEach, get, isFunction, isBoolean } = require('lodash');
3 | const { defaultsDeep } = require('@frctl/fractalite-support/utils');
4 | const createApp = require('@frctl/fractalite-app');
5 | const { createCompiler, createAdapter } = require('@frctl/fractalite-core');
6 | const { htmlAdapter } = require('@frctl/fractalite-core');
7 | const highlight = require('./src/server/utils/highlight');
8 | const markdown = require('./src/server/utils/markdown');
9 | const prettify = require('./src/server/utils/prettify');
10 | const createAssetResolver = require('./src/server/utils/resolve-asset');
11 | const plugins = require('./src/server/plugins');
12 |
13 | const configDefaults = {
14 | plugins: []
15 | };
16 |
17 | module.exports = function({ components, adapter, mode = {}, ...config }) {
18 | config = defaultsDeep(config, configDefaults);
19 |
20 | /*
21 | * App is (mostly) client side rendered app so need to make sure that
22 | * the permalink URLs are generated in the right format in both develop
23 | * and build modes.
24 | */
25 | mode.paths = mode.paths || {};
26 | mode.permalinks = mode.permalinks || {};
27 | mode.paths.ext = mode.paths.ext || '.html';
28 | mode.paths.indexes = isBoolean(mode.paths.indexes) ? mode.paths.indexes : true;
29 | mode.paths.prefix = false;
30 | mode.permalinks.indexes = false;
31 | mode.permalinks.ext = false;
32 | mode.permalinks.prefix = false;
33 |
34 | const compiler = createCompiler(components);
35 | const app = createApp(compiler, mode);
36 |
37 | adapter = adapter || htmlAdapter;
38 | adapter = isFunction(adapter) ? adapter(app, compiler) : adapter;
39 | adapter = createAdapter(compiler.getState(), adapter);
40 |
41 | app.addViewPath(resolve(__dirname, './views'));
42 |
43 | app.addStaticDir('app', resolve(__dirname, './dist'), '/app/assets');
44 | app.addStylesheet('app:app.css');
45 | app.addScript('app:app.js');
46 |
47 | app.addRoute('overview', '/', (ctx, next) => ctx.render('app'));
48 | app.addBuilder((state, { request }) => request({ name: 'overview' }));
49 |
50 | app.addRoute('api.component', '/api/components/:component.json', (ctx, next) => {
51 | ctx.body = ctx.component;
52 | return next();
53 | });
54 | app.addBuilder((state, { request }) => {
55 | state.components.forEach(component => {
56 | request({ name: 'api.component', params: { component } });
57 | });
58 | });
59 |
60 | /*
61 | * Add utilities
62 | */
63 | app.utils.highlightCode = highlight(get(config, 'opts.highlight'));
64 | app.utils.renderMarkdown = markdown({
65 | highlight: app.utils.highlightCode,
66 | ...get(config, 'opts.markdown')
67 | });
68 | app.utils.prettify = prettify(get(config, 'opts.prettify'));
69 | app.utils.resolveAsset = createAssetResolver(app);
70 |
71 | ['references', 'public', 'preview', 'pages', 'nav', 'inspector', 'theme', 'search'].forEach(name => {
72 | require(`./src/server/${name}`)(app, compiler, adapter, get(config, name));
73 | });
74 |
75 | /*
76 | * Load all core plugins, initialised with opts
77 | */
78 | plugins.forEach(({ key, handler }) => {
79 | const opts = get(config, key, {});
80 | const plugin = handler(opts);
81 | plugin(app, compiler, adapter);
82 | });
83 |
84 | /*
85 | * Load all user-defined plugins
86 | */
87 | forEach(config.plugins, plugin => plugin(app, compiler, adapter));
88 |
89 | /*
90 | * Add global props
91 | */
92 | app.addViewGlobal('app', {
93 | title: config.title || 'Fractalite',
94 | meta: defaultsDeep(config.meta || {}, {
95 | title: config.title || 'Fractalite',
96 | lang: 'en',
97 | dir: 'ltr',
98 | charset: 'utf-8',
99 | viewport: 'width=device-width, initial-scale=1.0'
100 | })
101 | });
102 |
103 | return app;
104 | };
105 |
--------------------------------------------------------------------------------
/packages/fractalite/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@frctl/fractalite-app": "^0.0.0",
6 | "@frctl/fractalite-core": "^0.0.0",
7 | "@frctl/fractalite-support": "^0.0.0",
8 | "asyncro": "^3.0.0",
9 | "axios": "^0.18.0",
10 | "common-tags": "^1.8.0",
11 | "escape-html": "^1.0.3",
12 | "flat": "^4.1.0",
13 | "gray-matter": "^4.0.1",
14 | "highlight.js": "^9.14.2",
15 | "ip": "^1.1.5",
16 | "kleur": "^2.0.2",
17 | "lodash": "^4.17.10",
18 | "markdown-it": "^8.4.2",
19 | "mri": "^1.1.1",
20 | "prettier": "^1.16.4",
21 | "signale": "^1.2.1",
22 | "socket.io-client": "^2.2.0"
23 | },
24 | "devDependencies": {
25 | "babel-core": "^6.26.3",
26 | "babel-preset-env": "^1.7.0",
27 | "codemirror": "^5.43.0",
28 | "normalize.css": "^8.0.0",
29 | "parcel-bundler": "^1.10.1",
30 | "sass": "^1.14.1",
31 | "split.js": "^1.5.10",
32 | "vue": "^2.6.8",
33 | "vue-awesome": "^3.3.1",
34 | "vue-codemirror": "^4.0.6",
35 | "vue-hot-reload-api": "^2.3.1",
36 | "vue-json-pretty": "1.4.1",
37 | "vue-resize-directive": "^1.1.4",
38 | "vue-router": "^3.0.2",
39 | "vue-socket.io-extended": "^3.2.1",
40 | "vuex": "^3.1.0",
41 | "vuex-persist": "^2.0.0"
42 | },
43 | "scripts": {
44 | "prepublish": "parcel build ./assets/app.js ./assets/reload.js --no-source-maps --log-level 2",
45 | "build": "parcel build ./assets/app.js ./assets/reload.js --no-source-maps",
46 | "dev": "parcel watch ./assets/app.js ./assets/reload.js --no-source-maps"
47 | },
48 | "bin": {
49 | "fractalite": "./bin/fractalite"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/fractalite/pages/index.njk:
--------------------------------------------------------------------------------
1 | ---
2 | title: Overview
3 | ---
4 |
5 | This is the landing page. You can override this page to customise your styleguide.
6 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/app.js:
--------------------------------------------------------------------------------
1 | /* global window, document */
2 |
3 | import Vue from 'vue/dist/vue';
4 | import VueSocketio from 'vue-socket.io-extended';
5 | import io from 'socket.io-client';
6 | import Navigation from './components/navigation';
7 | import Search from './components/search';
8 | import Error from './components/error';
9 | import JSONExplorer from './components/json-explorer';
10 | import SourceCode from './components/source-code';
11 | import AppLink from './components/app-link';
12 | import Split from './components/split';
13 | import router from './router';
14 | import store from './store';
15 | import eventBus from './events';
16 |
17 | Vue.use(VueSocketio, io(), { store });
18 |
19 | Vue.component('split-pane', Split);
20 | Vue.component('app-link', AppLink);
21 | Vue.component('json-explorer', JSONExplorer);
22 | Vue.component('source-code', SourceCode);
23 |
24 | window.app = new Vue({
25 | el: '#app',
26 | props: {
27 | loading: false
28 | },
29 | components: {
30 | Error,
31 | Navigation,
32 | Search
33 | },
34 | router,
35 | store,
36 | methods: {
37 | resetSearch() {
38 | this.$refs.search.resetSearch();
39 | }
40 | },
41 | computed: {
42 | navItems() {
43 | return this.$store.state.nav.items;
44 | },
45 | searchTargets() {
46 | return this.$store.state.search.targets;
47 | }
48 | },
49 | async mounted() {
50 | document.querySelector('body').classList.remove('loading');
51 |
52 | eventBus.$on('loading.start', () => {
53 | this.loading = true;
54 | });
55 |
56 | eventBus.$on('loading.stop', () => {
57 | this.loading = false;
58 | });
59 |
60 | await this.$store.dispatch('updateState');
61 |
62 | /*
63 | * Capture clicks in outside of Vue code and determine
64 | * whether to re-route them via vue-router.
65 | */
66 | window.addEventListener('click', event => {
67 | const { target } = event;
68 | if (target && target.matches("a:not([href*='://'])") && target.href) {
69 | if (target.matches('[href^="/preview/"]')) return;
70 | const { altKey, ctrlKey, metaKey, shiftKey, button, defaultPrevented } = event;
71 | if (metaKey || altKey || ctrlKey || shiftKey) return;
72 | if (defaultPrevented) return;
73 | if (button !== undefined && button !== 0) return;
74 | if (target && target.getAttribute) {
75 | const linkTarget = target.getAttribute('target');
76 | if (/\b_blank\b/i.test(linkTarget)) return;
77 | }
78 | const url = new URL(target.href);
79 | const to = url.pathname;
80 | const parts = target.href.split('/');
81 | if (window.location.pathname === to) {
82 | event.preventDefault();
83 | }
84 | if (parts[parts.length - 1].split('.').length === 1 && event.preventDefault) {
85 | event.preventDefault();
86 | this.$router.push(to);
87 | }
88 | }
89 | });
90 | }
91 | });
92 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/components/app-link.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'app-link',
3 | template: ``,
4 | props: {
5 | to: {
6 | type: String,
7 | required: true
8 | }
9 | },
10 | methods: {
11 | linkProps(url) {
12 | if (url.match(/^(http(s)?|ftp):\/\//)) {
13 | return { is: 'a', href: url, target: '_blank', rel: 'noopener' };
14 | }
15 | return { is: 'router-link', to: url };
16 | }
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/components/error.js:
--------------------------------------------------------------------------------
1 | export default {
2 | template: '#error',
3 | props: ['error'],
4 | computed: {
5 | name() {
6 | return this.error.name;
7 | },
8 | message() {
9 | return this.error.message;
10 | },
11 | stack() {
12 | return this.error.stack;
13 | }
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/components/inspector.js:
--------------------------------------------------------------------------------
1 | import eventBus from '../events';
2 | import Preview from './preview';
3 |
4 | export default {
5 | template: '#inspector',
6 | props: ['componentName', 'scenarioName'],
7 | components: { Preview },
8 | sockets: {
9 | async 'state.updated'() {
10 | await this.load();
11 | },
12 | refresh() {
13 | this.$refs.preview.reload();
14 | }
15 | },
16 | data() {
17 | return {
18 | component: null,
19 | scenario: {},
20 | preview: '',
21 | panels: [],
22 | currentTab: 0,
23 | loaded: false
24 | };
25 | },
26 | computed: {
27 | split() {
28 | return this.$store.state.inspector.split;
29 | }
30 | },
31 | methods: {
32 | async load() {
33 | this.loaded = false;
34 | this.preview = '';
35 | this.scenario = {};
36 | if (this.componentName) {
37 | try {
38 | const [component, inspectorData] = await Promise.all([
39 | await this.$store.dispatch('fetchComponent', this.componentName),
40 | await this.$store.dispatch('fetchInspectorData', {
41 | component: this.componentName,
42 | scenario: this.scenarioName
43 | })
44 | ]);
45 | this.component = component;
46 | const scenario = this.component.scenarios.find(scenario => scenario.name === this.scenarioName);
47 | if (!scenario) {
48 | const scenario = this.component.scenarios[0];
49 | this.$router.push(`/inspect/${this.component.name}/${scenario.name}`);
50 | return;
51 | }
52 | this.scenario = scenario;
53 | this.panels = inspectorData.panels;
54 | this.preview = inspectorData.preview;
55 | this.loaded = true;
56 | eventBus.$emit('loading.stop');
57 | } catch (err) {
58 | this.$store.commit('setError', err);
59 | }
60 | }
61 | },
62 | selectTab(i) {
63 | this.currentTab = i;
64 | },
65 | onDrawerResize(sizes) {
66 | this.$store.commit('setInspectorSplit', sizes);
67 | }
68 | },
69 | async mounted() {
70 | await this.load();
71 | },
72 | watch: {
73 | componentName() {
74 | return this.load();
75 | },
76 | scenarioName() {
77 | return this.load();
78 | }
79 | }
80 | };
81 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/components/json-explorer.js:
--------------------------------------------------------------------------------
1 | import VueJsonPretty from 'vue-json-pretty';
2 |
3 | export default {
4 | template: `
5 |
6 |
7 |
8 | `,
9 | props: ['data'],
10 | components: {
11 | VueJsonPretty
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/components/navigation.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue/dist/vue';
2 |
3 | export default {
4 | name: 'navigation',
5 | template: '#navigation',
6 | props: {
7 | items: Array,
8 | depth: {
9 | default: 1
10 | }
11 | },
12 | async mounted() {
13 | if (this.depth === 1) {
14 | const expandItems = items => {
15 | items.forEach(item => {
16 | if (item.expanded === true) {
17 | this.expand(item);
18 | }
19 | if (item.children) {
20 | expandItems(item.children);
21 | }
22 | });
23 | };
24 | expandItems(this.items);
25 | }
26 | this.$on('child-item-active', activeItem => {
27 | for (const item of this.items) {
28 | if (Array.isArray(item.children) && item.children.includes(activeItem)) {
29 | if (!this.isExpanded(item)) {
30 | this.toggleExpanded(item);
31 | }
32 | if (this.depth > 1) {
33 | this.$parent.$emit('child-item-active', item);
34 | }
35 | break;
36 | }
37 | }
38 | });
39 | for (const item of this.items) {
40 | if (item.url === this.$route.path) {
41 | this.$parent.$emit('child-item-active', item);
42 | break;
43 | }
44 | }
45 | },
46 | methods: {
47 | expand(item) {
48 | if (!this.isExpanded(item)) {
49 | this.$store.commit('toggleExpanded', item);
50 | }
51 | },
52 | toggleExpanded(item) {
53 | if (item.collapsable) {
54 | this.$store.commit('toggleExpanded', item);
55 | }
56 | },
57 | isExpanded(item) {
58 | if (item.collapsable) {
59 | return this.$store.getters.isExpanded(item);
60 | }
61 | return true;
62 | },
63 | handleClick(item) {
64 | if (item.url && (!item.children || (item.children && !this.isExpanded(item)))) {
65 | if (item.url.match(/^(http(s)?|ftp):\/\//)) {
66 | this.toggleExpanded(item);
67 | window.open(item.url, '_blank');
68 | } else {
69 | this.$router.push(item.url);
70 | }
71 | }
72 | if (item.children) {
73 | this.toggleExpanded(item);
74 | }
75 | }
76 | },
77 | watch: {
78 | $route(route) {
79 | for (const item of this.items) {
80 | if (item.url === route.path) {
81 | this.$parent.$emit('child-item-active', item);
82 | break;
83 | }
84 | }
85 | }
86 | }
87 | };
88 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/components/page.js:
--------------------------------------------------------------------------------
1 | import eventBus from '../events';
2 |
3 | export default {
4 | template: '#page',
5 | data() {
6 | return {
7 | page: null,
8 | content: null,
9 | loaded: false
10 | };
11 | },
12 | props: ['path'],
13 | methods: {
14 | async load() {
15 | if (this.path) {
16 | try {
17 | const { page, content } = await this.$store.dispatch('fetchPage', this.path);
18 | this.page = page;
19 | this.content = content;
20 | this.loaded = true;
21 | eventBus.$emit('loading.stop');
22 | } catch (err) {
23 | this.$store.commit('setError', err);
24 | }
25 | }
26 | }
27 | },
28 | async mounted() {
29 | await this.load();
30 | },
31 | watch: {
32 | async path() {
33 | this.loaded = false;
34 | await this.load();
35 | }
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/components/preview.js:
--------------------------------------------------------------------------------
1 | import resize from 'vue-resize-directive';
2 |
3 | let statsTimer = null;
4 |
5 | export default {
6 | template: '#preview',
7 | props: ['src', 'srcdoc'],
8 | data() {
9 | return {
10 | loaded: false,
11 | previewWidth: 0,
12 | previewHeight: 0,
13 | statsVisible: false
14 | };
15 | },
16 | computed: {
17 | split() {
18 | return this.$store.state.preview.split || [100, 0];
19 | }
20 | },
21 | directives: { resize },
22 | methods: {
23 | reload: debounce(
24 | function() {
25 | this.loaded = false;
26 | this.$refs.window.contentWindow.location.reload();
27 | },
28 | 500,
29 | true
30 | ),
31 | onPreviewResize(el) {
32 | this.previewWidth = el.clientWidth;
33 | this.previewHeight = el.clientHeight;
34 | this.statsVisible = true;
35 | if (statsTimer) {
36 | clearTimeout(statsTimer);
37 | }
38 | statsTimer = setTimeout(() => {
39 | this.statsVisible = false;
40 | }, 1000);
41 | },
42 | onDrawerResize(sizes) {
43 | this.$store.commit('setPreviewSplit', sizes);
44 | }
45 | },
46 | mounted() {
47 | this.loaded = false;
48 | this.previewWidth = this.$refs['preview-wrapper'].clientWidth;
49 | this.previewHeight = this.$refs['preview-wrapper'].clientHeight;
50 | this.$refs.window.addEventListener('load', () => {
51 | if (this.srcdoc) {
52 | this.loaded = true;
53 | }
54 | });
55 | },
56 | watch: {
57 | srcdoc() {
58 | this.loaded = false;
59 | },
60 | src() {
61 | this.loaded = false;
62 | }
63 | }
64 | };
65 |
66 | function debounce(func, wait, immediate) {
67 | let timeout;
68 | return function(...args) {
69 | const context = this;
70 | const later = function() {
71 | timeout = null;
72 | if (!immediate) func.apply(context, args);
73 | };
74 | const callNow = immediate && !timeout;
75 | clearTimeout(timeout);
76 | timeout = setTimeout(later, wait);
77 | if (callNow) func.apply(context, args);
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/components/search.js:
--------------------------------------------------------------------------------
1 | /* eslint array-callback-return:"off", no-labels: "off" */
2 |
3 | export default {
4 | name: 'search',
5 | template: '#search',
6 | props: ['components'],
7 | data() {
8 | return {
9 | searchTerm: ''
10 | };
11 | },
12 | computed: {
13 | isActive() {
14 | return this.term !== '';
15 | },
16 | term() {
17 | return this.searchTerm.trim().toLowerCase();
18 | },
19 | results() {
20 | if (this.isActive) {
21 | return this.components
22 | .map(component => {
23 | const scenarios = component.scenarios
24 | .map(scenario => {
25 | const matches = fuzzysearch(this.term, scenario.label.toLowerCase());
26 | if (matches) {
27 | return {
28 | ...scenario,
29 | label: highlight(scenario.label, matches)
30 | };
31 | }
32 | })
33 | .filter(scenario => scenario);
34 |
35 | const labelMatches = fuzzysearch(this.term, component.label.toLowerCase());
36 |
37 | let aliasMatches = [];
38 | if (component.aliases.length > 0) {
39 | aliasMatches = component.aliases
40 | .map(alias => {
41 | const matches = fuzzysearch(this.term, alias);
42 | if (matches) {
43 | return highlight(alias, matches);
44 | }
45 | })
46 | .filter(alias => alias);
47 | }
48 |
49 | if (labelMatches || aliasMatches.length > 0 || scenarios.length > 0) {
50 | return {
51 | scenarios: scenarios.length > 0 ? scenarios : component.scenarios,
52 | label: labelMatches ? highlight(component.label, labelMatches) : component.label,
53 | aliases: aliasMatches.join(', ')
54 | };
55 | }
56 | })
57 | .filter(component => component);
58 | }
59 | return [];
60 | }
61 | },
62 | methods: {
63 | resetSearch() {
64 | this.searchTerm = '';
65 | }
66 | }
67 | };
68 |
69 | // Adapted from https://github.com/bevacqua/fuzzysearch/blob/master/index.js
70 | function fuzzysearch(needle, haystack) {
71 | const hlen = haystack.length;
72 | const nlen = needle.length;
73 | const indexes = [];
74 | if (nlen > hlen) {
75 | return false;
76 | }
77 | if (nlen === hlen) {
78 | if (needle === haystack) {
79 | return Array.from({ length: needle.length }, (_, k) => k);
80 | }
81 | return false;
82 | }
83 | const pos = haystack.indexOf(needle);
84 | if (pos > -1) {
85 | return Array.from({ length: needle.length }, (_, k) => k + pos);
86 | }
87 | outer: for (let i = 0, j = 0; i < nlen; i++) {
88 | const nch = needle.charCodeAt(i);
89 | while (j < hlen) {
90 | if (haystack.charCodeAt(j++) === nch) {
91 | indexes.push(j - 1);
92 | continue outer;
93 | }
94 | }
95 | return false;
96 | }
97 | return indexes;
98 | }
99 |
100 | // Adapted from https://github.com/farzher/fuzzysort/blob/master/fuzzysort.js
101 | function highlight(target, indexes, hOpen, hClose) {
102 | if (hOpen === undefined) hOpen = '';
103 | if (hClose === undefined) hClose = '';
104 | let highlighted = '';
105 | let matchesIndex = 0;
106 | let opened = false;
107 | const targetLen = target.length;
108 | for (let i = 0; i < targetLen; ++i) {
109 | const char = target[i];
110 | if (indexes[matchesIndex] === i) {
111 | ++matchesIndex;
112 | if (!opened) {
113 | opened = true;
114 | highlighted += hOpen;
115 | }
116 | if (matchesIndex === indexes.length) {
117 | highlighted += char + hClose + target.substr(i + 1);
118 | break;
119 | }
120 | } else if (opened) {
121 | opened = false;
122 | highlighted += hClose;
123 | }
124 | highlighted += char;
125 | }
126 | return highlighted;
127 | }
128 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/components/source-code.js:
--------------------------------------------------------------------------------
1 | /* eslint import/no-unassigned-import: "off" */
2 |
3 | import { codemirror } from 'vue-codemirror';
4 |
5 | import 'codemirror/mode/css/css';
6 | import 'codemirror/mode/sass/sass';
7 | import 'codemirror/mode/yaml/yaml';
8 | import 'codemirror/mode/javascript/javascript';
9 | import 'codemirror/mode/htmlmixed/htmlmixed';
10 | import 'codemirror/addon/display/autorefresh';
11 |
12 | const defaultOpts = {
13 | lineNumbers: true,
14 | readOnly: true,
15 | lineWrapping: true,
16 | autoRefresh: true
17 | };
18 |
19 | export default {
20 | template: `
21 |
22 |
23 |
24 | `,
25 | props: ['options', 'value'],
26 | computed: {
27 | opts() {
28 | return { ...defaultOpts, ...this.options };
29 | }
30 | },
31 | components: { codemirror }
32 | };
33 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/components/split.js:
--------------------------------------------------------------------------------
1 | import split from 'split.js';
2 |
3 | export default {
4 | template: `
5 |
7 |
8 |
9 | `,
10 | props: {
11 | elements: {
12 | // Array of target split element ids
13 | type: Array,
14 | required: true,
15 | validator(value) {
16 | // Must be array of strings
17 | const isValid = value.every(i => typeof i === 'string');
18 | if (!isValid) {
19 | console.error(`VueSplitJs: Invalid elements - "${value}". Must be array of strings`);
20 | }
21 | return isValid;
22 | }
23 | },
24 | direction: {
25 | // Direction to split: horizontal or vertical.
26 | type: String,
27 | default: 'horizontal',
28 | validator(value) {
29 | const allowedValues = ['horizontal', 'vertical'];
30 | const isValid = allowedValues.includes(value);
31 | if (!isValid) {
32 | console.error(`VueSplitJs: Invalid direction - "${value}". Possible values are "horizontal" or "vertical"`);
33 | }
34 | return isValid;
35 | }
36 | },
37 | sizes: {
38 | // Initial sizes of each element in percents.
39 | type: Array,
40 | default: null
41 | },
42 | minSize: {
43 | // Minimum size of each element in pixels.
44 | type: [Number, Array],
45 | default: 100
46 | },
47 | gutterSize: {
48 | // Gutter size in pixels.
49 | type: Number,
50 | default: 10
51 | },
52 | snapOffset: {
53 | // Snap to minimum size offset.
54 | type: Number,
55 | default: 30
56 | },
57 | cursor: {
58 | // Cursor to display while dragging.
59 | type: String,
60 | default: 'grabbing'
61 | }
62 | },
63 | mounted() {
64 | split(this.elements, {
65 | direction: this.direction,
66 | sizes: this.sizes,
67 | minSize: this.minSize,
68 | gutterSize: this.gutterSize,
69 | snapOffset: this.snapOffset,
70 | cursor: this.cursor,
71 | onDrag: this.onDrag,
72 | onDragStart: this.onDragStart,
73 | onDragEnd: this.onDragEnd
74 | });
75 | },
76 | methods: {
77 | onDrag() {
78 | this.$emit('onDrag');
79 | },
80 | onDragEnd(sizes) {
81 | this.$emit('onDragEnd', sizes);
82 | },
83 | onDragStart() {
84 | this.$emit('onDragStart');
85 | }
86 | }
87 | };
88 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/events.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | export default new Vue();
4 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue/dist/vue';
2 | import VueRouter from 'vue-router';
3 | import Inspector from './components/inspector';
4 | import Page from './components/page';
5 | import eventBus from './events';
6 |
7 | Vue.use(VueRouter);
8 |
9 | const router = new VueRouter({
10 | mode: 'history',
11 | routes: [
12 | {
13 | name: 'inspect',
14 | path: '/inspect/:componentName/:scenarioName?',
15 | component: Inspector,
16 | props: true
17 | },
18 | {
19 | name: 'index',
20 | path: '/',
21 | component: Page,
22 | props: { path: 'index' }
23 | },
24 | {
25 | name: 'page',
26 | path: '/:path(.+)',
27 | component: Page,
28 | props: true
29 | }
30 | ]
31 | });
32 |
33 | router.beforeEach((to, from, next) => {
34 | eventBus.$emit('loading.start');
35 | next();
36 | });
37 |
38 | export default router;
39 |
--------------------------------------------------------------------------------
/packages/fractalite/src/client/store.js:
--------------------------------------------------------------------------------
1 | /* eslint camelcase:"off" */
2 |
3 | import Vue from 'vue/dist/vue';
4 | import Vuex from 'vuex';
5 | import axios from 'axios';
6 | import VuexPersistence from 'vuex-persist';
7 |
8 | Vue.use(Vuex);
9 |
10 | const persist = new VuexPersistence({
11 | reducer: state => ({
12 | nav: {
13 | expandedIds: state.nav.expandedIds
14 | },
15 | preview: {
16 | split: state.preview.split
17 | },
18 | inspector: {
19 | split: state.inspector.split
20 | }
21 | })
22 | });
23 |
24 | export default new Vuex.Store({
25 | plugins: [persist.plugin],
26 |
27 | state: {
28 | components: [],
29 | pages: [],
30 | inspector: {
31 | data: [],
32 | split: null
33 | },
34 | nav: {
35 | items: [],
36 | expandedIds: []
37 | },
38 | search: {
39 | targets: []
40 | },
41 | preview: {
42 | split: null
43 | },
44 | error: null,
45 | initialised: false
46 | },
47 |
48 | mutations: {
49 | setNavigationItems(state, items) {
50 | state.nav.items = items;
51 | },
52 |
53 | toggleExpanded(state, item) {
54 | const { id } = item;
55 | const index = state.nav.expandedIds.indexOf(id);
56 | if (index > -1) {
57 | state.nav.expandedIds.splice(index, 1);
58 | } else {
59 | state.nav.expandedIds.push(id);
60 | }
61 | },
62 |
63 | setSearchTargets(state, items) {
64 | state.search.targets = items;
65 | },
66 |
67 | clearComponentCache(state) {
68 | state.components = [];
69 | },
70 |
71 | clearPageCache(state) {
72 | state.pages = [];
73 | },
74 |
75 | clearInspectorDataCache(state) {
76 | state.inspector.data = [];
77 | },
78 |
79 | setComponent(state, component) {
80 | state.components.push(component);
81 | },
82 |
83 | setPage(state, page) {
84 | state.pages.push(page);
85 | },
86 |
87 | setInspectorData(state, inspectorData) {
88 | state.inspector.data.push(inspectorData);
89 | },
90 |
91 | setInspectorSplit(state, sizes) {
92 | state.inspector.split = sizes;
93 | },
94 |
95 | setPreviewSplit(state, sizes) {
96 | state.preview.split = sizes;
97 | },
98 |
99 | setError(state, err) {
100 | if (err.status === 404 || err.response) {
101 | return;
102 | }
103 | state.error = err;
104 | },
105 |
106 | clearError(state) {
107 | state.error = null;
108 | },
109 |
110 | initialised(state) {
111 | state.initialised = true;
112 | }
113 | },
114 |
115 | actions: {
116 | async socket_stateUpdated({ commit, dispatch }) {
117 | return dispatch('updateState');
118 | },
119 |
120 | socket_err({ commit }, err) {
121 | commit('setError', err[0]);
122 | },
123 |
124 | async updateState({ commit }) {
125 | try {
126 | commit('clearComponentCache');
127 | commit('clearPageCache');
128 | commit('clearInspectorDataCache');
129 | const [navigation, search] = await Promise.all([await axios.get('/api/navigation.json'), await axios.get('/api/search.json')]);
130 | commit('setNavigationItems', navigation.data.items);
131 | commit('setSearchTargets', search.data.components);
132 | commit('clearError');
133 | commit('initialised', true);
134 | } catch (err) {
135 | commit('setError', err);
136 | throw err;
137 | }
138 | },
139 |
140 | async fetchComponent({ commit, getters }, componentName) {
141 | try {
142 | const fetchedComponent = getters.getComponent(componentName);
143 | if (fetchedComponent) {
144 | return fetchedComponent;
145 | }
146 | const request = await axios.get(`/api/components/${componentName}.json`);
147 | const component = request.data;
148 | commit('setComponent', component);
149 | return component;
150 | } catch (err) {
151 | commit('setError', err);
152 | throw err;
153 | }
154 | },
155 |
156 | async fetchPage({ commit, getters }, path) {
157 | try {
158 | const fetchedPage = getters.getPage(path);
159 | if (fetchedPage) {
160 | return fetchedPage;
161 | }
162 | const request = await axios.get(`/api/pages/${path}.json`);
163 | const page = request.data;
164 | commit('setPage', page);
165 | return page;
166 | } catch (err) {
167 | commit('setError', err);
168 | throw err;
169 | }
170 | },
171 |
172 | async fetchInspectorData({ commit, getters }, { component, scenario }) {
173 | try {
174 | const fetchedInspectorData = getters.getInspectorData(component, scenario);
175 | if (fetchedInspectorData) {
176 | return fetchedInspectorData;
177 | }
178 | const request = await axios.get(`/api/inspect/${component}/${scenario}.json`);
179 | const data = {
180 | id: `${component}/${scenario}`,
181 | ...request.data
182 | };
183 | commit('setInspectorData', data);
184 | return data;
185 | } catch (err) {
186 | commit('setError', err);
187 | throw err;
188 | }
189 | }
190 | },
191 |
192 | getters: {
193 | isExpanded: state => item => {
194 | return state.nav.expandedIds.indexOf(item.id) > -1;
195 | },
196 |
197 | getComponent: state => name => {
198 | return state.components.find(component => component.name === name);
199 | },
200 |
201 | getPage: state => path => {
202 | return state.pages.find(page => page.path === path);
203 | },
204 |
205 | getInspectorData: state => (componentName, scenarioName) => {
206 | return state.inspector.data.find(data => data.id === `${componentName}/${scenarioName}`);
207 | }
208 | }
209 | });
210 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/inspector.js:
--------------------------------------------------------------------------------
1 | const { orderBy, merge, isString } = require('lodash');
2 | const { titlize, resolveValue, mapValuesAsync } = require('@frctl/fractalite-support/utils');
3 | const { getScenarioOrDefault, getScenario, getComponent } = require('@frctl/fractalite-core/helpers');
4 | const { map } = require('asyncro');
5 |
6 | module.exports = function(app, compiler, adapter, opts = {}) {
7 | let panels = [];
8 |
9 | app.extend({
10 | addInspectorPanel(panel) {
11 | if (typeof panel.name !== 'string') {
12 | throw new TypeError(`Inspector panels must specify a .name property`);
13 | }
14 |
15 | const panelDefaults = {
16 | label: titlize(panel.name),
17 | position: panels.length + 1,
18 | renderServer: true,
19 | renderClient: false,
20 | props: {},
21 | template: ''
22 | };
23 |
24 | panel = merge({}, panelDefaults, panel);
25 |
26 | if (isString(panel.templateFile)) {
27 | panel.template = async () => {
28 | const tpl = await app.views.getTemplateAsync(panel.templateFile);
29 | return tpl.tmplStr;
30 | };
31 | }
32 |
33 | if (panel.renderServer) {
34 | const tpl = panel.template;
35 | panel.template = async ctx => {
36 | const tplString = await resolveValue(tpl, ctx);
37 | return app.utils.renderPage(tplString, { ...ctx, panel }, { template: true });
38 | };
39 | }
40 |
41 | if (isString(panel.css)) {
42 | app.addCSS(panel.css);
43 | }
44 |
45 | if (isString(panel.js)) {
46 | app.addJS(panel.js);
47 | }
48 |
49 | panels.push(panel);
50 | return app;
51 | },
52 |
53 | removeInspectorPanel(name) {
54 | panels = panels.filter(p => p.name !== name);
55 | return app;
56 | },
57 |
58 | getInspectorPanels() {
59 | return orderBy(panels, 'position', 'asc');
60 | }
61 | });
62 |
63 | app.addRoute('api.inspect', '/api/inspect/:component/:scenario.json', async (ctx, next) => {
64 | const { component } = ctx;
65 | let scenario;
66 |
67 | try {
68 | scenario = getScenarioOrDefault(component, ctx.params.scenario);
69 | } catch (err) {
70 | err.status = 404;
71 | throw err;
72 | }
73 |
74 | const panels = await map(app.getInspectorPanels(), panel => {
75 | return mapValuesAsync(panel, value => resolveValue(value, { ...ctx.state, scenario, component }));
76 | });
77 |
78 | ctx.body = {
79 | preview: await app.utils.renderPreview(component, scenario),
80 | panels: panels.filter(panel => panel.template)
81 | };
82 |
83 | return next();
84 | });
85 |
86 | app.addBuilder((state, { request }) => {
87 | state.components.forEach(component => {
88 | component.scenarios.forEach(scenario => {
89 | request({ name: 'api.inspect', params: { component, scenario: scenario.name } });
90 | });
91 | });
92 | });
93 |
94 | app.addRoute('inspect', '/inspect/:component/:scenario?', (ctx, next) => ctx.render('app'));
95 |
96 | /*
97 | * Compiler middleware to add inspector url properties
98 | */
99 | compiler.use(async (components, next) => {
100 | await next();
101 | components.forEach(component => {
102 | component.url = app.url('inspect', { component: component.name, scenario: component.scenarios[0].name });
103 | component.scenarios.forEach(scenario => {
104 | scenario.url = app.url('inspect', { component: component.name, scenario: scenario.name });
105 | });
106 | });
107 | });
108 |
109 | app.utils.addReferenceLookup('inspect', (state, identifier) => {
110 | const [componentName, scenarioName] = identifier.split('/');
111 | const component = getComponent(state, componentName, true);
112 | const scenario = scenarioName ? getScenario(component, scenarioName, true) : component.scenarios[0];
113 | return {
114 | url: app.url('inspect', { component, scenario: scenario.name })
115 | };
116 | });
117 |
118 | return app;
119 | };
120 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/nav.js:
--------------------------------------------------------------------------------
1 | const { dirname } = require('path');
2 | const { merge, isBoolean, isFunction, isPlainObject, flatMap, uniqBy, orderBy, compact } = require('lodash');
3 | const { titlize, slugify } = require('@frctl/fractalite-support/utils');
4 | const { isComponent, isFile } = require('@frctl/fractalite-core/helpers');
5 |
6 | module.exports = function(app, compiler, adapter, opts = {}) {
7 | const defaults = {
8 | label: str => str,
9 | items: defaultGenerator,
10 | scenarios: true
11 | };
12 |
13 | if (Array.isArray(opts) || isFunction(opts)) {
14 | opts = { items: opts };
15 | }
16 |
17 | opts = merge(defaults, opts);
18 |
19 | app.addRoute('api.navigation', '/api/navigation.json', ctx => {
20 | ctx.body = {
21 | opts,
22 | items: buildNav(opts.items, ctx.state)
23 | };
24 | });
25 |
26 | app.addBuilder((state, { request }) => request({ name: 'api.navigation' }));
27 |
28 | compiler.use(components => {
29 | components.forEach(component => {
30 | component.position = component.config.position || 1000;
31 | });
32 | });
33 |
34 | function generateLabel(str, target) {
35 | return isFunction(opts.label) ? opts.label(str, target) : str;
36 | }
37 |
38 | function buildNav(items, state) {
39 | items = isFunction(items) ? items(state, toTree) : items;
40 | return expandValues(items);
41 | }
42 |
43 | function expandValues(items, level = 0) {
44 | return flatMap(compact(items), item => {
45 | if (Array.isArray(item)) {
46 | return expandValues(item, level);
47 | }
48 |
49 | if (item.entity) {
50 | item = item.entity;
51 | }
52 |
53 | const entry = {};
54 |
55 | if (isComponent(item)) {
56 | Object.assign(entry, {
57 | id: `component-${item.name}`,
58 | type: 'component',
59 | label: item.label,
60 | url: item.scenarios[0].url
61 | });
62 | if (opts.scenarios) {
63 | entry.children = item.scenarios.map(scenario => {
64 | const entry = {
65 | id: `scenario-${item.name}-${scenario.name}`,
66 | type: 'scenario',
67 | url: scenario.url
68 | };
69 | entry.label = generateLabel(scenario.label, entry, scenario);
70 | return entry;
71 | });
72 | }
73 | } else if (isFile(item)) {
74 | Object.assign(entry, {
75 | id: item.handle,
76 | type: 'file',
77 | url: entry.url,
78 | label: item.handle
79 | });
80 | } else {
81 | Object.assign(entry, {
82 | url: item.url,
83 | children: item.children ? expandValues(item.children, level + 1) : null,
84 | label: item.label || item.handle
85 | });
86 | }
87 |
88 | if (isPlainObject(item.url)) {
89 | const { route, props } = item.url;
90 | entry.url = app.url(route, props);
91 | }
92 |
93 | entry.label = generateLabel(entry.label, entry, item);
94 | entry.id = slugify(entry.id || item.id || item.url || item.path || `level-${level}-${entry.label}`);
95 | entry.collapsable = isBoolean(item.collapsable) ? item.collapsable : true;
96 | entry.expanded = isBoolean(item.expanded) ? item.expanded : false;
97 |
98 | return entry;
99 | });
100 | }
101 | };
102 |
103 | function defaultGenerator({ components, pages }, toTree) {
104 | return [
105 | toTree(pages),
106 | components.length === 0
107 | ? null
108 | : {
109 | label: 'Components',
110 | children: toTree(components)
111 | }
112 | ];
113 | }
114 |
115 | function toTree(items, props = {}, pathProp) {
116 | let nodes = flatMap(items, item => {
117 | const path = item.treePath || item[pathProp] || '';
118 | const nodes = makeNodes(path.trim('/'));
119 | const leaf = nodes[nodes.length - 1];
120 | leaf.entity = item;
121 | leaf.position = item.position || leaf.position;
122 | return nodes;
123 | });
124 | nodes = orderBy(uniqBy(nodes, 'path'), ['position'], ['asc']);
125 |
126 | return nodes
127 | .filter(node => node.depth === 1)
128 | .map(node => {
129 | node.children = getChildNodes(node, nodes);
130 | return node;
131 | });
132 | }
133 |
134 | function getChildNodes(parent, nodes) {
135 | const children = nodes.filter(node => {
136 | return node.depth === parent.depth + 1 && dirname(node.path) === parent.path;
137 | });
138 | for (const child of children) {
139 | child.children = getChildNodes(child, nodes);
140 | }
141 | return children;
142 | }
143 |
144 | function makeNodes(path = '') {
145 | const nodes = [];
146 | let tmpPath;
147 | const segments = [];
148 | for (const segment of path.split('/')) {
149 | const [, position = 10000, name] = segment.match(/^(?:(\d+)-)?(.*)$/);
150 | tmpPath = tmpPath ? `${tmpPath}/${segment}` : segment;
151 | segments.push(name);
152 | nodes.push({
153 | name,
154 | position: parseInt(position, 10),
155 | label: titlize(name),
156 | path: tmpPath,
157 | depth: segments.length
158 | });
159 | }
160 | return nodes;
161 | }
162 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/pages.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 | const { find, uniqBy, cloneDeep, isBoolean } = require('lodash');
3 | const { normalizeSrc } = require('@frctl/fractalite-support/utils');
4 | const { titlize } = require('@frctl/fractalite-support/utils');
5 | const { read, watch } = require('@frctl/fractalite-core');
6 | const { map } = require('asyncro');
7 | const matter = require('gray-matter');
8 |
9 | module.exports = function(app, compiler, adapter, opts = {}) {
10 | if (typeof opts === 'string' || Array.isArray(opts)) opts = { src: opts };
11 | opts = cloneDeep(opts);
12 | opts.src = normalizeSrc(opts.src);
13 | opts.src.paths.push(resolve(__dirname, '../../pages'));
14 |
15 | let pages = [];
16 |
17 | app.utils.addReferenceLookup('page', (state, identifier) => {
18 | let page = state.pages.find(page => page.handle === identifier);
19 | if (!page) {
20 | page = state.pages.find(page => page.url === identifier);
21 | }
22 | return page;
23 | });
24 |
25 | app.utils.parseFrontMatter = function(content, opts = {}) {
26 | return matter({ ...opts, content });
27 | };
28 |
29 | app.utils.renderPage = async function(str = '', props = {}, opts = {}) {
30 | const state = compiler.getState();
31 | if (opts.template === true) {
32 | str = await app.views.renderStringAsync(str, { ...state, ...props });
33 | }
34 | if (opts.refs === true) {
35 | str = app.utils.parseRefs(str);
36 | }
37 | if (opts.markdown === true) {
38 | str = app.utils.renderMarkdown(str);
39 | }
40 | return str;
41 | };
42 |
43 | app.addRoute('api.page', '/api/pages/:path(.+).json', async (ctx, next) => {
44 | const page = find(pages, { indexPath: ctx.params.path });
45 | if (page) {
46 | const content = await app.utils.renderPage(
47 | await page.getContents(),
48 | { page },
49 | {
50 | markdown: page.markdown,
51 | template: page.template,
52 | refs: page.refs
53 | }
54 | );
55 | ctx.body = { path: ctx.params.path, page, content };
56 | }
57 | return next();
58 | });
59 |
60 | app.addBuilder((state, { request }) => {
61 | state.pages.forEach(page => {
62 | request({ name: 'api.page', params: { path: page.indexPath } });
63 | });
64 | });
65 |
66 | app.addRoute('page', ':path(.*)', async (ctx, next) => {
67 | const page = find(pages, { urlPath: ctx.params.path });
68 | if (page) {
69 | await ctx.render('app');
70 | }
71 | return next();
72 | });
73 |
74 | app.beforeStart(async (app, state) => {
75 | try {
76 | if (app.mode === 'develop') {
77 | pages = await getPages();
78 | state.pages = pages;
79 | watch(opts.src.paths, { ignoreInitial: true }).on('all', async (event, path) => {
80 | const hrStart = process.hrtime();
81 | try {
82 | pages = await getPages();
83 | state.pages = pages;
84 | const time = process.hrtime(hrStart);
85 | app.emit('state.updated', state, { event, path, time });
86 | } catch (err) {
87 | app.emit('error', err);
88 | }
89 | });
90 | } else if (app.mode === 'build') {
91 | pages = await getPages();
92 | state.pages = pages;
93 | }
94 | } catch (err) {
95 | app.emit('error', err);
96 | }
97 | });
98 |
99 | async function getPages() {
100 | const files = await read(opts.src.paths, { ...opts.src.opts, onlyFiles: true });
101 | pages = await map(files, async file => {
102 | const { data, content } = app.utils.parseFrontMatter(await file.getContents(), opts.frontmatter);
103 | return makePage(file, content, data);
104 | });
105 | return uniqBy(pages, 'url');
106 | }
107 |
108 | function makePage(file, content, data) {
109 | const page = { ...data };
110 |
111 | const segments = file.relative
112 | .replace(file.extname, '')
113 | .split('/')
114 | .map(segment => segment.match(/^(?:(\d+)-)?(.*)$/)[2]);
115 |
116 | let urlPath = segments.join('/');
117 | if (urlPath === 'index' || /^.*\/index$/.test(urlPath)) {
118 | if (urlPath === 'index') {
119 | urlPath = '';
120 | }
121 | page.index = true;
122 | page.label = page.label || opts.indexLabel || 'Overview';
123 | page.position = page.position || 1;
124 | } else {
125 | page.label = page.label || titlize(file.name);
126 | }
127 |
128 | page.handle = page.handle || urlPath.replace('/', '-');
129 | if (page.handle === '') {
130 | page.handle = 'index';
131 | }
132 |
133 | page.title = page.title || page.label;
134 | page.treePath = urlPath;
135 | page.urlPath = '/' + urlPath;
136 | page.indexPath = (urlPath + (page.index ? '/index' : '')).replace(/^\//, '');
137 |
138 | page.url = app.url('page', { path: page.urlPath });
139 |
140 | page.layout = typeof page.layout === 'boolean' ? page.layout : true;
141 |
142 | page.refs = isBoolean(page.refs) ? page.refs : true;
143 | page.markdown = isBoolean(page.markdown) ? page.markdown : ['.md', '.markdown'].includes(file.ext);
144 | page.template = isBoolean(page.template) ? page.template : file.ext === '.njk';
145 |
146 | page.raw = content;
147 | page.isPage = true;
148 |
149 | page.getContents = function() {
150 | return Promise.resolve(page.raw);
151 | };
152 | return page;
153 | }
154 | };
155 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/plugins/index.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | key: 'inspector.info',
4 | handler: require('./inspector-info')
5 | },
6 | {
7 | key: 'inspector.html',
8 | handler: require('./inspector-html')
9 | },
10 | {
11 | key: 'inspector.props',
12 | handler: require('./inspector-props')
13 | }
14 | ];
15 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/plugins/inspector-html.js:
--------------------------------------------------------------------------------
1 | /* eslint camelcase: "off" */
2 |
3 | module.exports = function(opts = {}) {
4 | return function inspectorHTMLPlugin(app, compiler, adapter) {
5 | if (opts === false) return;
6 |
7 | const prettify = opts.prettify !== false;
8 |
9 | app.addInspectorPanel({
10 | name: 'html',
11 | label: opts.label || 'HTML',
12 | renderClient: true,
13 | template: `
14 |
15 | `,
16 | async props(state) {
17 | const { scenario, component } = state;
18 | let items = await adapter.renderAllToStaticMarkup(component, scenario.preview.props);
19 | items = items.map(item => (prettify ? app.utils.prettify(item, 'html') : item));
20 | return { html: items };
21 | }
22 | });
23 | };
24 | };
25 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/plugins/inspector-info.js:
--------------------------------------------------------------------------------
1 | module.exports = function(opts = {}) {
2 | return function inspectorInfoPlugin(app) {
3 | if (opts === false) return;
4 |
5 | const className = 'inspector-panel-info';
6 |
7 | app.addInspectorPanel({
8 | name: 'info',
9 | label: opts.label,
10 | props: { className },
11 | templateFile: 'plugins/inspector-info',
12 | css: `
13 | .${className} {
14 | padding: 12px;
15 | }
16 | .${className}__header {
17 | display: flex;
18 | border-bottom: 1px solid #ddd;
19 | padding-bottom: 12px;
20 | margin-bottom: 20px;
21 | }
22 | .${className}__title {
23 | font-size: 20px;
24 | }
25 | .${className}__proptable a {
26 | text-decoration: none;
27 | }
28 | `
29 | });
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/plugins/inspector-props.js:
--------------------------------------------------------------------------------
1 | module.exports = function(opts = {}) {
2 | return function inspectorPropsPlugin(app) {
3 | if (opts === false) return;
4 |
5 | app.addInspectorPanel({
6 | name: 'props',
7 | label: opts.label || 'Props',
8 | renderClient: true,
9 | template: `
10 |
11 | `,
12 | props(state) {
13 | const { scenario } = state;
14 | const { props } = scenario.preview;
15 | return props.length > 1 ? props : props[0];
16 | }
17 | });
18 | };
19 | };
20 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/public.js:
--------------------------------------------------------------------------------
1 | module.exports = function(app, compiler, adapter, opts = {}) {
2 | if (typeof opts === 'string') {
3 | opts = {
4 | path: opts
5 | };
6 | }
7 |
8 | if (opts.path) {
9 | app.addStaticDir('public', opts.path, opts.mount);
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/references.js:
--------------------------------------------------------------------------------
1 | module.exports = function(app, compiler, adapter, opts = {}) {
2 | if (opts === false) return;
3 |
4 | const refMatcher = opts.match || /{(.*?)}/g;
5 |
6 | const lookup = {
7 | component(state, identifier) {
8 | return state.components.find(c => c.name === identifier);
9 | },
10 | file(state, identifier) {
11 | return state.files.find(c => c.handle === identifier);
12 | }
13 | };
14 |
15 | app.utils.addReferenceLookup = (key, handler) => {
16 | lookup[key] = handler;
17 | return app;
18 | };
19 |
20 | app.utils.parseRefs = str => {
21 | return str.replace(refMatcher, (matched, ref) => {
22 | const refParts = ref.split(':');
23 |
24 | const [entity, identifier, prop = 'url'] = refParts;
25 |
26 | if (!lookup[entity]) {
27 | throw new Error(`Could not resolve reference tag - '${entity}' is not a recognised entity`);
28 | }
29 | try {
30 | const target = lookup[entity](compiler.getState(), identifier);
31 | if (!target) {
32 | throw new Error(`Could not resolve reference tag - '${entity}:${identifier}' not found`);
33 | }
34 | return target[prop] || '';
35 | } catch (err) {
36 | app.emit('error', err);
37 | return '';
38 | }
39 | });
40 | };
41 | };
42 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/search.js:
--------------------------------------------------------------------------------
1 | const { flatMap } = require('lodash');
2 |
3 | module.exports = function(app, compiler, adapter, opts = {}) {
4 | app.addRoute('api.search', '/api/search.json', ctx => {
5 | const components = flatMap(ctx.components, component => {
6 | const { search } = component;
7 | if (search.hidden) {
8 | return;
9 | }
10 | return {
11 | label: component.label,
12 | url: component.url,
13 | aliases: search.aliases || [],
14 | scenarios: component.scenarios.map(scenario => {
15 | return {
16 | label: scenario.label,
17 | url: scenario.url
18 | };
19 | })
20 | };
21 | }).filter(component => component);
22 | ctx.body = { opts, components };
23 | });
24 |
25 | compiler.use(components => {
26 | const defaults = {
27 | hidden: false,
28 | aliases: []
29 | };
30 | components.forEach(component => {
31 | const searchOpts = component.config.search || {};
32 | component.search = { ...defaults, ...searchOpts };
33 | });
34 | });
35 |
36 | app.addBuilder((state, { request }) => request({ name: 'api.search' }));
37 | };
38 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/theme.js:
--------------------------------------------------------------------------------
1 | const { isString, isFunction, isPlainObject, forEach } = require('lodash');
2 |
3 | module.exports = function(app, compiler, adapter, opts = {}) {
4 | if (isFunction(opts)) {
5 | opts = opts(app) || {};
6 | }
7 |
8 | if (isString(opts.css)) {
9 | app.addCSS(opts.css);
10 | }
11 |
12 | if (isString(opts.js)) {
13 | app.addCSS(opts.js);
14 | }
15 |
16 | if (Array.isArray(opts.stylesheets)) {
17 | opts.stylesheets.forEach(stylesheet => {
18 | const { path, url } = app.utils.resolveAsset(stylesheet);
19 | app.addStylesheet(url, path);
20 | });
21 | }
22 |
23 | if (Array.isArray(opts.scripts)) {
24 | opts.scripts.forEach(script => {
25 | const { path, url } = app.utils.resolveAsset(script);
26 | app.addScript(url, path);
27 | });
28 | }
29 |
30 | if (isString(opts.views)) {
31 | app.addViewPath(opts.views);
32 | }
33 |
34 | if (isPlainObject(opts.vars)) {
35 | let css = ':root {\n';
36 | forEach(opts.vars, (val, key) => {
37 | css += `--${key}: ${val};\n`;
38 | });
39 | css += '}';
40 | app.addCSS(css);
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/utils/highlight.js:
--------------------------------------------------------------------------------
1 | const hljs = require('highlight.js');
2 | const escape = require('escape-html');
3 |
4 | module.exports = function() {
5 | return function highlightCode(str, lang = 'html') {
6 | if (lang && hljs.getLanguage(lang)) {
7 | try {
8 | const h = hljs.highlight(lang, str, true);
9 | return `${h.value}
`;
10 | } catch (err) {}
11 | }
12 | return `${escape(str)}
`;
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/utils/markdown.js:
--------------------------------------------------------------------------------
1 | const { defaultsDeep } = require('@frctl/fractalite-support/utils');
2 | const Markdown = require('markdown-it');
3 |
4 | module.exports = function(opts = {}) {
5 | const md = new Markdown(
6 | defaultsDeep(opts, {
7 | html: true,
8 | linkify: true,
9 | highlight: opts.highlight
10 | })
11 | );
12 |
13 | function renderMarkdown(str) {
14 | return md.render(str);
15 | }
16 |
17 | renderMarkdown.engine = md;
18 |
19 | return renderMarkdown;
20 | };
21 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/utils/prettify.js:
--------------------------------------------------------------------------------
1 | const prettier = require('prettier');
2 |
3 | module.exports = function() {
4 | return function prettify(str, opts = 'babel') {
5 | if (typeof opts === 'string') {
6 | opts = { parser: opts };
7 | }
8 | if (!opts.parser || opts.parser === 'js' || opts.parser === 'json') {
9 | opts.parser = 'babel';
10 | }
11 | return prettier.format(str, opts);
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/packages/fractalite/src/server/utils/resolve-asset.js:
--------------------------------------------------------------------------------
1 | const { basename } = require('path');
2 | const { isPlainObject, isString } = require('lodash');
3 | const { normalizePath } = require('@frctl/fractalite-support/utils');
4 |
5 | module.exports = function(app) {
6 | return function resolveAsset(asset) {
7 | let path;
8 | let url;
9 | if (isString(asset)) {
10 | if (asset.startsWith('//') || asset.includes('://')) {
11 | url = asset; // Full URL, use as-is
12 | } else if (asset.includes(':')) {
13 | // Reference a file in a static directory
14 | url = app.resourceUrl(asset);
15 | } else {
16 | // Assume it's a path
17 | path = asset;
18 | }
19 | } else if (isPlainObject(asset)) {
20 | path = asset.path;
21 | url = asset.url;
22 | if (!path && !url) {
23 | throw new Error(`Cannot add asset - either a .url or a .path property must be defined`);
24 | }
25 | } else {
26 | throw new Error(`Cannot add asset - either a string or an object description is required`);
27 | }
28 | path = path ? normalizePath(path) : path;
29 | if (!url) {
30 | url = `/${basename(path)}`;
31 | }
32 | return { path, url };
33 | };
34 | };
35 |
--------------------------------------------------------------------------------
/packages/fractalite/views/app.njk:
--------------------------------------------------------------------------------
1 | {% extends "layout" %}
2 | {% set loading = true %}
3 |
4 | {% block body %}
5 |
6 |
7 |
21 |
22 |
23 |
24 |
25 | {% endblock %}
26 |
27 | {% block preScripts %}
28 |
32 | {% for template in ['navigation', 'preview', 'inspector', 'page', 'error', 'search'] %}
33 |
36 | {% endfor %}
37 | {% endblock %}
38 |
--------------------------------------------------------------------------------
/packages/fractalite/views/error.njk:
--------------------------------------------------------------------------------
1 | {% extends "layout" %}
2 |
3 | {% block body %}
4 |
5 |
6 |
{{ error.title | default('An error occured') }}
7 |
{{ error.message }}
8 | {% if error.status != 404 %}
9 |
{{ error.stack }}
10 | {% endif %}
11 |
12 |
13 | {% endblock %}
14 |
15 | {% block postScripts %}
16 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/packages/fractalite/views/layout.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {%- block preStyles %}{% endblock %}
8 | {%- block styles %}
9 | {% for url in stylesheets -%}
10 |
11 | {%- endfor %}
12 | {% endblock -%}
13 | {% block postStyles %}{% endblock -%}
14 |
15 | {% block title %}{% if page.title %}{{ page.title }} | {% endif %}{{ app.meta.title }}{% endblock %}
16 |
17 |
18 |
19 |
20 | {%- block preBody %}{% endblock %}
21 | {% block body %}{% endblock %}
22 | {%- block postbody %}{% endblock %}
23 |
24 |
25 | {%- block preScripts %}{% endblock %}
26 | {%- block scripts %}
27 | {% for url in scripts -%}
28 |
29 | {%- endfor %}
30 | {% endblock -%}
31 | {%- block postScripts %}{% endblock -%}
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/packages/fractalite/views/partials/brand.njk:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/packages/fractalite/views/plugins/inspector-info.njk:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
Scenarios:
7 |
8 | {% for scene in component.scenarios %}
9 |
17 | {% endfor %}
18 |
19 |
Files:
20 |
21 | {% for file in component.files %}
22 |
23 |
24 | {{ file.componentPath }}
25 |
26 |
27 | {{ file.size }}
28 |
29 |
32 |
33 | {% endfor %}
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/packages/fractalite/views/preview.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% for url in stylesheets %}
7 |
8 | {% endfor %}
9 | {% if css %}
10 |
13 | {% endif %}
14 | {% block title %}{{ meta.title | default(component.label + ' Preview') }}{% endblock %}
15 |
16 |
17 | {% block content %}
18 | {{ content | safe }}
19 | {% endblock %}
20 | {% for url in scripts %}
21 |
22 | {% endfor %}
23 | {% if js %}
24 |
27 | {% endif %}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/packages/fractalite/views/vue/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ name }}
4 |
{{ message }}
5 |
{{ stack }}
6 |
7 |
8 |
--------------------------------------------------------------------------------
/packages/fractalite/views/vue/inspector.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
25 |
26 |
--------------------------------------------------------------------------------
/packages/fractalite/views/vue/navigation.html:
--------------------------------------------------------------------------------
1 |
2 | -
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/packages/fractalite/views/vue/page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ page.title }}
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/packages/fractalite/views/vue/preview.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
{{ previewWidth }}px x {{ previewHeight }}px
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/packages/fractalite/views/vue/search.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ×
5 |
6 |
7 |
8 | No matching components
9 |
10 |
11 | -
12 |
13 |
14 |
15 | []
16 |
17 |
18 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/packages/plugin-assets-bundler/README.md:
--------------------------------------------------------------------------------
1 | ## @frctl/fractalite-plugin-assets-bundler
2 |
3 | Uses [Parcel](https://parceljs.org) to handle asset bundling for Fractal component libraries.
4 |
5 | #### 1. Install in your project:
6 |
7 | ```bash
8 | npm i --save-dev @frctl/fractalite-plugin-assets-bundler
9 | ```
10 |
11 | #### 2. Add plugin to Fractal config file:
12 |
13 | ```js
14 | // fractal.config.js
15 | module.exports = {
16 | // ...
17 | plugins: [
18 | require('@frctl/fractalite-plugin-assets-bundler')({
19 | entryFile: './src/preview.js',
20 | outFile: './dist/build.js'
21 | })
22 | ]
23 | };
24 | ```
25 |
26 | #### 3. Create entry file:
27 |
28 | ```js
29 | // ./assets/preview.js
30 | import '../components/**/*.scss'
31 | import button from '../components/@button/button.js'
32 | ```
33 |
34 | > See the Parcel docs on module resolution for more info on paths, globbing and aliases: https://parceljs.org/module_resolution.html
35 |
36 | #### 4. View your components...
37 |
38 | The asset bundler handles serving assets and injecting them into your component previews so no further configuration is needed.
39 |
--------------------------------------------------------------------------------
/packages/plugin-assets-bundler/index.js:
--------------------------------------------------------------------------------
1 | const { dirname, basename, relative } = require('path');
2 | const slash = require('slash');
3 | const Bundler = require('parcel-bundler');
4 | const prettier = require('prettier');
5 | const { outputFile } = require('fs-extra');
6 |
7 | module.exports = function(opts = {}) {
8 | return function(app) {
9 | app.beforeStart(async () => {
10 | const entryDir = dirname(opts.entryFile);
11 | const outDir = dirname(opts.outFile);
12 | const publicUrl = opts.publicUrl || '/assets';
13 |
14 | // Serve any assets generated in the output dir
15 | app.addStaticDir('bundled-assets', outDir, publicUrl);
16 |
17 | // If an entry generator is provided, use that to
18 | // dynamically create (and re-create) the entry file
19 | // when the compiler state changes
20 | await generateEntry(app.compiler.getState());
21 |
22 | let bundler = await createBundler(true);
23 |
24 | // When glob imports are used Parcel does not
25 | // correctly recognise when files are added/removed,
26 | // only when changed. This is the equivalent of manually
27 | // stopping and restarting the watch task/hmr server.
28 | app.on('state.updated', async (state, { event }) => {
29 | if (event === 'add' || event === 'unlink') {
30 | if (bundler) {
31 | try {
32 | await bundler.stop();
33 | } catch (err) {
34 | app.emit(err);
35 | }
36 | await generateEntry(state);
37 | bundler = await createBundler();
38 | app.socket.broadcast('refresh');
39 | }
40 | }
41 | });
42 |
43 | async function generateEntry(state) {
44 | if (typeof opts.entryBuilder !== 'function') {
45 | return;
46 | }
47 | try {
48 | const entry = await opts.entryBuilder(state, { dir: entryDir });
49 | await outputFile(opts.entryFile, prettier.format(entry, { parser: 'babel' }));
50 | } catch (err) {
51 | app.emit('error', err);
52 | }
53 | }
54 |
55 | async function createBundler(initial = false) {
56 | const bundler = new Bundler(opts.entryFile, {
57 | outDir,
58 | outFile: basename(opts.outFile),
59 | watch: app.mode === 'develop',
60 | logLevel: 3,
61 | publicUrl,
62 | hmrHostname: opts.hmrHostname || 'localhost',
63 | hmr: app.mode === 'develop' ? opts.hmr !== false : false
64 | });
65 |
66 | if (initial) {
67 | bundler.on('buildEnd', () => {
68 | if (opts.hmr === false) {
69 | app.socket.broadcast('refresh');
70 | }
71 | });
72 | }
73 |
74 | const bundle = await bundler.bundle();
75 |
76 | if (initial) {
77 | const bundles = addBundles(bundle);
78 |
79 | const assets = bundles.map(bundle => {
80 | return {
81 | url: `${publicUrl}/${slash(relative(outDir, bundle.name))}`,
82 | type: bundle.type
83 | };
84 | });
85 |
86 | if (opts.addToPreview !== false) {
87 | // Automatically add the bundled scripts
88 | // and stylesheets to the preview
89 | for (const { type, url } of assets) {
90 | if (type === 'css') {
91 | app.addPreviewStylesheet(url);
92 | }
93 | if (type === 'js') {
94 | app.addPreviewScript(url);
95 | }
96 | }
97 | }
98 | }
99 |
100 | return bundler;
101 |
102 | function addBundles(bundle, bundles = []) {
103 | if (['js', 'css'].includes(bundle.type)) {
104 | bundles.push(bundle);
105 | }
106 | for (const childBundle of bundle.childBundles) {
107 | addBundles(childBundle, bundles);
108 | }
109 | return bundles;
110 | }
111 | }
112 | });
113 | };
114 | };
115 |
--------------------------------------------------------------------------------
/packages/plugin-assets-bundler/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite-plugin-assets-bundler",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "fs-extra": "^7.0.1",
6 | "parcel-bundler": "^1.11.0",
7 | "prettier": "^1.16.4",
8 | "slash": "^2.0.0"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/plugin-notes/README.md:
--------------------------------------------------------------------------------
1 | ## @frctl/fractalite-plugin-notes
2 |
3 | Adds an inspector panel to display component notes.
4 |
--------------------------------------------------------------------------------
/packages/plugin-notes/index.js:
--------------------------------------------------------------------------------
1 | const { defaultsDeep } = require('@frctl/fractalite-support/utils');
2 | const { rewriteUrls } = require('@frctl/fractalite-support/html');
3 | const stripIndent = require('strip-indent');
4 |
5 | module.exports = function(opts = {}) {
6 | return function inspectorNotesPlugin(app, compiler) {
7 | if (opts.notesFile) {
8 | /*
9 | * Compiler middleware to read notes content from notes files
10 | */
11 | compiler.use(async components => {
12 | const filePath = typeof opts.notesFile === 'string' ? opts.notesFile : 'notes.md';
13 | await Promise.all(
14 | components.map(async component => {
15 | const notesFile = component.files.find(file => file.relative === filePath);
16 | if (notesFile) {
17 | component.notes = await notesFile.getContents();
18 | }
19 | })
20 | );
21 | });
22 | }
23 |
24 | /*
25 | * Compiler middleware to extract notes from config.
26 | * Will override notes from the notesFile if supplied.
27 | */
28 | compiler.use(components => {
29 | components.forEach(component => {
30 | if (component.config.notes) {
31 | component.notes = stripIndent(component.config.notes);
32 | }
33 | });
34 | });
35 |
36 | app.addInspectorPanel({
37 | name: 'notes',
38 | label: opts.label,
39 | renderServer: false,
40 | async template(state) {
41 | const { component } = state;
42 | let notes;
43 |
44 | if (typeof component.notes === 'string') {
45 | /*
46 | * Notes are parsed in the same way as pages and can contain frontmatter
47 | * and reference tags.
48 | */
49 | const { content, data } = app.utils.parseFrontMatter(component.notes);
50 |
51 | const renderOpts = defaultsDeep(data, {
52 | markdown: true,
53 | template: false,
54 | refs: true
55 | });
56 |
57 | notes = await app.utils.renderPage(content, {}, renderOpts);
58 |
59 | // Rewrite relative URLs to src files in output
60 | notes = rewriteUrls(notes, path => {
61 | if (path.startsWith('./')) {
62 | const file = component.files.find(file => `./${file.relative}` === path);
63 | return file ? app.url('src', { file }) : path;
64 | }
65 | });
66 | } else {
67 | notes = 'No notes available';
68 | }
69 |
70 | return `${notes}
`;
71 | },
72 |
73 | css: `
74 | .inspector-panel-notes {
75 | padding: 12px;
76 | }
77 | `
78 | });
79 | };
80 | };
81 |
--------------------------------------------------------------------------------
/packages/plugin-notes/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite-plugin-notes",
3 | "version": "0.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "strip-indent": {
8 | "version": "2.0.0",
9 | "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz",
10 | "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g="
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/plugin-notes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite-plugin-notes",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@frctl/fractalite-support": "^0.0.0",
6 | "strip-indent": "^2.0.0"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/support/README.md:
--------------------------------------------------------------------------------
1 | ## @frctl/fractalite-support
2 |
3 | Utilities and helpers for Fractalite package development.
4 |
--------------------------------------------------------------------------------
/packages/support/html.js:
--------------------------------------------------------------------------------
1 | const attributes = require('html-url-attributes');
2 |
3 | const urlAttrs = new RegExp(
4 | // eslint-disable-next-line no-useless-escape
5 | `(${Object.keys(attributes).join('|')})\=([\"\'])([^\"\']*)([\"\'])`,
6 | 'gi'
7 | );
8 |
9 | module.exports = {
10 | rewriteUrls(str, replacer) {
11 | return str.replace(urlAttrs, (...args) => {
12 | const [matched, attr, quoteOpen, path, quoteClose] = args;
13 | const newPath = replacer(path, { attr, matched, quoteOpen, quoteClose });
14 | if (newPath) {
15 | return `${attr}=${quoteOpen}${newPath}${quoteClose}`;
16 | }
17 | return matched;
18 | });
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/packages/support/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | utils: require('./utils'),
3 | html: require('./html'),
4 | loadConfig: require('./load-config')
5 | };
6 |
--------------------------------------------------------------------------------
/packages/support/load-config.js:
--------------------------------------------------------------------------------
1 | const cosmiconfig = require('cosmiconfig');
2 |
3 | module.exports = async function(opts = {}) {
4 | let configFile;
5 |
6 | if (opts.path) {
7 | const configFinder = cosmiconfig(opts.name || 'fractal');
8 | configFile = await configFinder.load(opts.path);
9 | } else {
10 | const configFinder = cosmiconfig(opts.name || 'fractal', {
11 | stopDir: opts.cwd || process.cwd()
12 | });
13 | configFile = await configFinder.search();
14 | }
15 |
16 | if (!configFile) {
17 | const error = new Error('Config file not found.');
18 | error.type = 'CONFIG_NOT_FOUND';
19 | throw error;
20 | }
21 |
22 | return {
23 | path: configFile.filepath,
24 | config: configFile.config
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/packages/support/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite-support",
3 | "version": "0.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "argparse": {
8 | "version": "1.0.10",
9 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
10 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
11 | "requires": {
12 | "sprintf-js": "~1.0.2"
13 | }
14 | },
15 | "caller-callsite": {
16 | "version": "2.0.0",
17 | "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
18 | "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=",
19 | "requires": {
20 | "callsites": "^2.0.0"
21 | }
22 | },
23 | "caller-path": {
24 | "version": "2.0.0",
25 | "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz",
26 | "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=",
27 | "requires": {
28 | "caller-callsite": "^2.0.0"
29 | }
30 | },
31 | "callsites": {
32 | "version": "2.0.0",
33 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
34 | "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA="
35 | },
36 | "cosmiconfig": {
37 | "version": "5.0.7",
38 | "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.7.tgz",
39 | "integrity": "sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA==",
40 | "requires": {
41 | "import-fresh": "^2.0.0",
42 | "is-directory": "^0.3.1",
43 | "js-yaml": "^3.9.0",
44 | "parse-json": "^4.0.0"
45 | }
46 | },
47 | "error-ex": {
48 | "version": "1.3.2",
49 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
50 | "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
51 | "requires": {
52 | "is-arrayish": "^0.2.1"
53 | }
54 | },
55 | "esprima": {
56 | "version": "4.0.1",
57 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
58 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
59 | },
60 | "html-url-attributes": {
61 | "version": "1.0.0",
62 | "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-1.0.0.tgz",
63 | "integrity": "sha1-e0yOlalf3pk6N1vhoWK7LLBY58I="
64 | },
65 | "import-fresh": {
66 | "version": "2.0.0",
67 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
68 | "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=",
69 | "requires": {
70 | "caller-path": "^2.0.0",
71 | "resolve-from": "^3.0.0"
72 | }
73 | },
74 | "is-arrayish": {
75 | "version": "0.2.1",
76 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
77 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
78 | },
79 | "is-directory": {
80 | "version": "0.3.1",
81 | "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz",
82 | "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE="
83 | },
84 | "js-yaml": {
85 | "version": "3.12.1",
86 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz",
87 | "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==",
88 | "requires": {
89 | "argparse": "^1.0.7",
90 | "esprima": "^4.0.0"
91 | }
92 | },
93 | "json-parse-better-errors": {
94 | "version": "1.0.2",
95 | "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
96 | "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
97 | },
98 | "lodash": {
99 | "version": "4.17.11",
100 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
101 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
102 | },
103 | "parse-json": {
104 | "version": "4.0.0",
105 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
106 | "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
107 | "requires": {
108 | "error-ex": "^1.3.1",
109 | "json-parse-better-errors": "^1.0.1"
110 | }
111 | },
112 | "resolve-from": {
113 | "version": "3.0.0",
114 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
115 | "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g="
116 | },
117 | "slash": {
118 | "version": "2.0.0",
119 | "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
120 | "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="
121 | },
122 | "slugify": {
123 | "version": "1.3.1",
124 | "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.1.tgz",
125 | "integrity": "sha512-6BwyhjF5tG5P8s+0DPNyJmBSBePG6iMyhjvIW5zGdA3tFik9PtK+yNkZgTeiroCRGZYgkHftFA62tGVK1EI9Kw=="
126 | },
127 | "sprintf-js": {
128 | "version": "1.0.3",
129 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
130 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
131 | },
132 | "titlecase": {
133 | "version": "1.1.2",
134 | "resolved": "https://registry.npmjs.org/titlecase/-/titlecase-1.1.2.tgz",
135 | "integrity": "sha1-eBE9EQgIa4MmMxoyR96o9aSeqFM="
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/packages/support/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frctl/fractalite-support",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "cosmiconfig": "^5.0.6",
6 | "html-url-attributes": "^1.0.0",
7 | "lodash": "^4.17.10",
8 | "slash": "^2.0.0",
9 | "slugify": "^1.3.1",
10 | "titlecase": "^1.1.2"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/support/utils.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const _ = require('lodash');
3 | const slug = require('slugify');
4 | const titlecase = require('titlecase');
5 | const slash = require('slash');
6 |
7 | const utils = {
8 | addTrailingSeparator(str) {
9 | return path.join(str, path.sep);
10 | },
11 |
12 | removeTrailingSeparator(str) {
13 | while (utils.endsInSeparator(str)) {
14 | str = str.slice(0, -1);
15 | }
16 | return str;
17 | },
18 |
19 | endsInSeparator(str) {
20 | const last = str[str.length - 1];
21 | return str.length > 1 && (last === '/' || (utils.isWin() && last === '\\'));
22 | },
23 |
24 | normalizePath(filePath, cwd) {
25 | // Assert.string(filePath, `Path must be a string. Received '${typeof filePath}' [paths-invalid]`);
26 | cwd = cwd || process.cwd();
27 | if (!path.isAbsolute(filePath)) {
28 | filePath = path.join(cwd, filePath);
29 | }
30 | filePath = utils.removeTrailingSeparator(filePath);
31 | return path.normalize(filePath);
32 | },
33 |
34 | normalizePaths(paths, cwd) {
35 | paths = utils.toArray(paths);
36 | return paths.map(filePath => utils.normalizePath(filePath, cwd));
37 | },
38 |
39 | normalizeName(str) {
40 | return _.kebabCase(str.toLowerCase().replace(/^[^a-z]*/, ''));
41 | },
42 |
43 | normalizeExt(ext) {
44 | return `.${ext.toLowerCase().replace(/^\./, '')}`;
45 | },
46 |
47 | permalinkify(str, opts = {}) {
48 | const indexes = opts.indexes || false;
49 | let permalink = _.trim(slash(str), '/');
50 | if (opts.ext !== false) {
51 | const fallbackExt = utils.normalizeExt(opts.ext || '.html');
52 | permalink = permalink === '' ? 'index' + fallbackExt : permalink;
53 | if (!path.extname(permalink)) {
54 | permalink += (indexes && !/\/?index$/.test(permalink) ? '/index' : '') + fallbackExt;
55 | }
56 | }
57 | if (opts.prefix) {
58 | permalink = _.trim(opts.prefix, '/') + '/' + permalink;
59 | }
60 | return '/' + permalink;
61 | },
62 |
63 | slugify(str, replacement) {
64 | return slug(str.toLowerCase(), replacement || '-');
65 | },
66 |
67 | titlize(str) {
68 | return titlecase(str.replace(/[-_]/g, ' '));
69 | },
70 |
71 | isWin() {
72 | return process.platform === 'win32';
73 | },
74 |
75 | defaultsDeep(...args) {
76 | const output = {};
77 | let customizer;
78 | args = _.compact(args);
79 | if (typeof args[args.length - 1] === 'function') {
80 | const fn = args.pop();
81 | customizer = (defaultValue, targetValue, ...other) => fn(targetValue, defaultValue, ...other);
82 | } else {
83 | customizer = (defaultValue, targetValue) => {
84 | if (Array.isArray(targetValue)) {
85 | // Don't merge arrays - the target array overrides the default value
86 | return targetValue;
87 | }
88 | if (_.isFunction(defaultValue) && _.isObject(targetValue)) {
89 | return targetValue;
90 | }
91 | };
92 | }
93 | const items = args.reverse().map(item => _.cloneDeep(item));
94 | items.forEach(item => _.mergeWith(output, item, customizer));
95 | return output;
96 | },
97 |
98 | assign(target, ...sources) {
99 | sources.forEach(source => {
100 | const descriptors = Object.keys(source).reduce((descriptors, key) => {
101 | descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
102 | return descriptors;
103 | }, {});
104 | Object.getOwnPropertySymbols(source).forEach(sym => {
105 | const descriptor = Object.getOwnPropertyDescriptor(source, sym);
106 | if (descriptor.enumerable) {
107 | descriptors[sym] = descriptor;
108 | }
109 | });
110 | Object.defineProperties(target, descriptors);
111 | });
112 | return target;
113 | },
114 |
115 | toArray(args) {
116 | if (args === null || args === undefined) {
117 | return [];
118 | }
119 | return [].concat(args);
120 | },
121 |
122 | resolveValue(value, ctx) {
123 | return Promise.resolve(_.isFunction(value) ? value(ctx) : value);
124 | },
125 |
126 | async mapValuesAsync(obj, asyncFn) {
127 | const keys = Object.keys(obj);
128 | const promises = keys.map(async key => {
129 | return { key, value: await asyncFn(obj[key]) };
130 | });
131 | const values = await Promise.all(promises);
132 | const newObj = {};
133 | values.forEach(v => {
134 | newObj[v.key] = v.value;
135 | });
136 | return newObj;
137 | },
138 |
139 | normalizeSrc(src, defaults = {}) {
140 | if (_.isString(src) || Array.isArray(src)) {
141 | src = {
142 | paths: utils.toArray(src)
143 | };
144 | }
145 | src = utils.defaultsDeep(src, defaults, { opts: {} });
146 | src.paths = utils.normalizePaths(src.paths);
147 | return src;
148 | }
149 | };
150 |
151 | module.exports = utils;
152 |
--------------------------------------------------------------------------------
/test/helpers/generate-components.js:
--------------------------------------------------------------------------------
1 | const { resolve, join } = require('path');
2 | const { outputFile } = require('fs-extra');
3 | const { source, html } = require('common-tags');
4 | const { map } = require('asyncro');
5 |
6 | const testComponentsDir = resolve(__dirname, '../fixtures/generated');
7 |
8 | const nestedProps = JSON.stringify(
9 | {
10 | level1: {
11 | level2: {
12 | level3: {
13 | item1: 'foo',
14 | item2: 'bar',
15 | item3: ['foo', 'bar', 'baz']
16 | }
17 | }
18 | }
19 | },
20 | null,
21 | 2
22 | );
23 |
24 | const svgExpand = ``;
25 | const svgCollapse = '';
26 |
27 | async function generateComponents(componentCount = 100) {
28 | const nums = Array.from({ length: componentCount }, (_, k) => k + 1);
29 | return map(nums, count => {
30 | const name = `component-${count}`;
31 | const dirPath = join(testComponentsDir, `@${name}`);
32 | const configContents = source`
33 | module.exports = {
34 | position: ${count},
35 | scenarios: [
36 | {
37 | name: 'component-${count}-1',
38 | props: {
39 | num: '${count}-1',
40 | collapseIcon: './collapse.svg',
41 | nested: ${nestedProps}
42 | }
43 | },
44 | {
45 | name: 'component-${count}-2',
46 | props: {
47 | num: '${count}-2',
48 | collapseIcon: './collapse.svg',
49 | nested: ${nestedProps}
50 | }
51 | },
52 | {
53 | name: 'component-${count}-3',
54 | props: {
55 | num: '${count}-3',
56 | collapseIcon: './collapse.svg',
57 | nested: ${nestedProps}
58 | }
59 | }
60 | ]
61 | };
62 | `;
63 | const viewContents = html`
64 |
65 |
Component {num}
66 |

67 |

68 |
69 | `;
70 | return Promise.all([
71 | outputFile(join(dirPath, `${name}.config.js`), configContents),
72 | outputFile(join(dirPath, `view.html`), viewContents),
73 | outputFile(join(dirPath, `expand.svg`), svgExpand),
74 | outputFile(join(dirPath, `collapse.svg`), svgCollapse)
75 | ]);
76 | });
77 | }
78 |
79 | (async () => {
80 | await generateComponents(100);
81 | console.log('Components generated');
82 | })();
83 |
--------------------------------------------------------------------------------