├── .editorconfig
├── .github
├── FUNDING.yml
└── workflows
│ ├── comment-issue.yml
│ └── tests.yml
├── .gitignore
├── .husky
├── .gitignore
└── pre-commit
├── .idea
├── .gitignore
├── inspectionProfiles
│ └── Project_Default.xml
├── jsLibraryMappings.xml
├── markdown.xml
├── meteor-fast-render.iml
├── modules.xml
├── vcs.xml
└── workspace.xml
├── .markdownlint.json
├── .markdownlintignore
├── .meteorignore
├── .versions
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── lib
├── client
│ ├── auth.js
│ ├── boot.js
│ ├── ddp_update.js
│ ├── fast_render.js
│ └── id_tools.js
└── server
│ ├── context.js
│ ├── fast_render.js
│ ├── publish_context.js
│ ├── routes.js
│ └── utils.js
├── package-lock.json
├── package.js
├── package.json
├── testApp
├── .gitignore
├── .meteor
│ ├── .finished-upgraders
│ ├── .gitignore
│ ├── .id
│ ├── packages
│ ├── platforms
│ ├── release
│ └── versions
├── package-lock.json
└── package.json
└── tests
├── client
├── ddp_update.js
├── fast_render.js
├── index.js
└── utils.js
└── server
├── context.js
├── index.js
└── integration.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | end_of_line = lf
3 | insert_final_newline = true
4 |
5 | [*.{js,jsx,html,json}]
6 | indent_style = space
7 | indent_size = 2
8 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [copleykj]
4 | patreon: copleykj
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/workflows/comment-issue.yml:
--------------------------------------------------------------------------------
1 | name: Add immediate comment on new issues
2 |
3 | on:
4 | issues:
5 | types: [opened]
6 |
7 | jobs:
8 | createComment:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Create Comment
12 | uses: peter-evans/create-or-update-comment@v1.4.2
13 | with:
14 | issue-number: ${{ github.event.issue.number }}
15 | body: |
16 | Thank you for submitting this issue!
17 |
18 | We, the Members of Meteor Community Packages take every issue seriously.
19 | Our goal is to provide long-term lifecycles for packages and keep up
20 | with the newest changes in Meteor and the overall NodeJs/JavaScript ecosystem.
21 |
22 | However, we contribute to these packages mostly in our free time.
23 | Therefore, we can't guarantee your issues to be solved within certain time.
24 |
25 | If you think this issue is trivial to solve, don't hesitate to submit
26 | a pull request, too! We will accompany you in the process with reviews and hints
27 | on how to get development set up.
28 |
29 | Please also consider sponsoring the maintainers of the package.
30 | If you don't know who is currently maintaining this package, just leave a comment
31 | and we'll let you know
32 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - develop
8 | - CITests
9 | pull_request:
10 | branches:
11 | - master
12 | - develop
13 |
14 | jobs:
15 | tests:
16 | name: tests
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: checkout
20 | uses: actions/checkout@v4
21 |
22 | - name: Setup meteor
23 | uses: meteorengineer/setup-meteor@v1
24 |
25 | - name: cache dependencies
26 | uses: actions/cache@v3
27 | with:
28 | path: ~/.npm
29 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
30 | restore-keys: |
31 | ${{ runner.os }}-node-
32 |
33 | - run: npm ci
34 | - run: npm run lint
35 | - run: cd testApp && meteor npm ci
36 | - run: cd testApp && meteor npm run test
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .tmp-qa
2 | .build*
3 | .npm
4 | test/packages/fast-render
5 | versions.json
6 | node_modules
7 | .eslintcache
8 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run pre-commit
5 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/jsLibraryMappings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/markdown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/meteor-fast-render.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {
25 | "associatedIndex": 5
26 | }
27 |
28 |
29 |
30 |
31 |
32 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | 1692806842234
68 |
69 |
70 | 1692806842234
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "default": true,
3 | "MD013": false
4 | }
5 |
--------------------------------------------------------------------------------
/.markdownlintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | testApp
3 |
--------------------------------------------------------------------------------
/.meteorignore:
--------------------------------------------------------------------------------
1 | testApp
2 |
--------------------------------------------------------------------------------
/.versions:
--------------------------------------------------------------------------------
1 | accounts-base@2.2.11
2 | allow-deny@1.1.1
3 | babel-compiler@7.10.5
4 | babel-runtime@1.5.1
5 | base64@1.0.12
6 | binary-heap@1.0.11
7 | boilerplate-generator@1.7.2
8 | callback-hook@1.5.1
9 | check@1.4.1
10 | communitypackages:fast-render@4.0.9
11 | communitypackages:inject-data@2.3.3
12 | communitypackages:picker@1.2.0
13 | ddp@1.4.1
14 | ddp-client@2.6.2
15 | ddp-common@1.4.1
16 | ddp-rate-limiter@1.2.1
17 | ddp-server@2.7.1
18 | diff-sequence@1.1.2
19 | dynamic-import@0.7.3
20 | ecmascript@0.16.8
21 | ecmascript-runtime@0.8.1
22 | ecmascript-runtime-client@0.12.1
23 | ecmascript-runtime-server@0.11.0
24 | ejson@1.1.3
25 | fetch@0.1.4
26 | geojson-utils@1.0.11
27 | http@1.4.2
28 | id-map@1.1.1
29 | inter-process-messaging@0.1.1
30 | livedata@1.0.18
31 | local-test:communitypackages:fast-render@4.0.9
32 | localstorage@1.2.0
33 | logging@1.3.4
34 | meteor@1.11.5
35 | meteortesting:browser-tests@0.1.2
36 | meteortesting:mocha@0.4.4
37 | minimongo@1.9.4
38 | modern-browsers@0.1.10
39 | modules@0.20.0
40 | modules-runtime@0.13.1
41 | mongo@1.16.10
42 | mongo-decimal@0.1.3
43 | mongo-dev-server@1.1.0
44 | mongo-id@1.0.8
45 | montiapm:meteorx@2.3.1
46 | npm-mongo@4.17.2
47 | ordered-dict@1.1.0
48 | practicalmeteor:mocha-core@1.0.1
49 | promise@0.12.2
50 | random@1.2.1
51 | rate-limit@1.1.1
52 | react-fast-refresh@0.2.8
53 | reactive-var@1.0.12
54 | reload@1.3.1
55 | retry@1.1.0
56 | routepolicy@1.1.1
57 | server-render@0.4.1
58 | socket-stream-client@0.5.2
59 | tracker@1.3.3
60 | typescript@4.9.5
61 | underscore@1.6.1
62 | url@1.3.2
63 | webapp@1.13.8
64 | webapp-hashing@1.1.1
65 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## 4.0.9
4 |
5 | - Updated `communitypackages:picker` to v1.2.0
6 | - Updated `communitypackages:inject-data` to v2.3.3
7 | - Updated development dependencies
8 | - Updated Meteor compatibility upgrade to v3.0-rc.1, note that this does not mean that this package is Meter 3 compatible, only that it won't give you trouble in version resolution when updating to RC 1.
9 |
10 | ## 4.0.7 & 4.0.8
11 |
12 | - Update `montiapm:meteorx` to v2.3.1
13 | - Minor update of dev depenenices
14 | - Updated GitHub Actions workflow
15 | - Update `cookie-parser` to v1.4.6
16 |
17 | ## 4.0.6
18 |
19 | - Update dependencies
20 |
21 | ## 4.0.5
22 |
23 | - Update meteorx to `montiapm:meteorx` for compatibility with Meteor 2.3
24 |
25 | ## 4.0.2
26 |
27 | - Compatibility with Meteor 2.3
28 |
29 | ## 4.0.1
30 |
31 | - Fixed null publication data not being added to payload
32 |
33 | ## 4.0.0
34 |
35 | - Added FastRender.onDataReady callback
36 | - Added Extra Data API
37 | - Removed dependency on underscore
38 | - Improved code structure and refactored to modern syntax
39 | - Removed dependence on outdated chuangbo:cookie package
40 | - Removed the reliance on load order for timing of data loading and the need for `FastRender.wait()`
41 |
42 | ## 3.3.1
43 |
44 | - published as `communitypackages:fast-render`
45 | - uses newly created `communitypackages:inject-data`
46 |
47 | ## 3.3.0
48 |
49 | - Fix for Meteor 1.9 (thanks @mrauhu)
50 |
51 | ## 3.2.0
52 |
53 | - Flush buffered DDP writes for every message instead of batching them together, so that one failure doesn't cause them all to fail. There is a race condition between the `Accounts` package and `FastRender.init` in development mode. FastRender's buffered writes will fail if `Accounts` adds the `user` document to the collection after FR's checks, but before/during the batched buffered write. This change makes the buffered writes atomic and adds better error messages so we can tell specifically which write has failed.
54 |
55 | ## 3.0.8
56 |
57 | - Fixed params serialization issue that would cause server-side subscriptions (via `route`, `onAllRoutes`, `onPageLoad`) to not match their client-side counterparts if `onStop` or `onReady` callback usage did not match in both environments
58 |
59 | ## 3.0.7
60 |
61 | - Revert regression from 3.0.5
62 |
63 | ## 3.0.6
64 |
65 | - Upgrade `inject-data` dependency version
66 |
67 | ## 3.0.5
68 |
69 | - Switched to `lamhieu:meteorx` from `meteorhacks:meteorx`
70 | - Fixed an issue where stringified server-side subscription params would not match their client-side counterparts when using SSR
71 |
72 | ## 3.0.4
73 |
74 | - De-duplicate Package.onUse to fix issue with Meteor 1.7.0.1 (thanks @banjerluke)
75 |
76 | ## 3.0.3
77 |
78 | - Add support for an async callback in `FastRender.onPageLoad`
79 |
80 | ## 3.0.2
81 |
82 | - Fix server-render dependency
83 |
84 | ## 3.0.1
85 |
86 | - Add weak dependency on server-render
87 |
88 | ## v3.0.0
89 |
90 | - Added SSR helper APIs
91 | - Removed support for iron router
92 | - Updated for Meteor 1.6.1
93 |
94 | ## v2.16.5
95 |
96 | - Fixed "document already present for an add" issue (thanks @anubhav756)
97 |
98 | ## v2.16.4
99 |
100 | - Update for Meteor 1.5+ (thanks @hexsprite)
101 |
102 | ## v2.16.3
103 |
104 | - Updating `staringatlights:inject-data` dependency
105 |
106 | ## v2.16
107 |
108 | - Releasing as `staringatlights:fast-render`
109 |
110 | ## v2.14.0
111 |
112 | - Add support for Meteor 1.3.2 with buffered DDP. See [PR167](https://github.com/kadirahq/fast-render/pull/167)
113 |
114 | ## v2.13.0
115 |
116 | - Use a real subscription as the Publication context. See [PR160](https://github.com/kadirahq/fast-render/pull/160).
117 |
118 | ## v2.12.0
119 |
120 | - Use inject-data's 2.0 API.
121 |
122 | ## v2.11.0
123 |
124 | - Refactor the way how context process data. With this, we can fix [this](https://github.com/kadirahq/flow-router/issues/431) FlowRouter SSR issue.
125 |
126 | ## v2.10.0
127 |
128 | - Throw an exception, when a route start without "/". See: [#135](https://github.com/meteorhacks/fast-render/pull/135)
129 |
130 | ## v2.9.0
131 |
132 | - Add support for Meteor 1.2
133 |
134 | ## v2.8.1
135 |
136 | - Fix some integration tests
137 |
138 | ## v2.8.0
139 |
140 | - Add more internal APIs to support SSR
141 |
142 | ## v2.5.1
143 |
144 | - Add some updates to DeepCopy function
145 |
146 | ## v2.5.0
147 |
148 | - Add IE8 Support
149 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing To Fast Render
2 |
3 | - [Introduction](#introduction)
4 | - [Your First Contribution](#your-first-contribution)
5 | - [Submitting code](#submitting-code)
6 | - [Code review process](#code-review-process)
7 | - [Financial contributions](#financial-contributions)
8 | - [Questions](#questions)
9 |
10 | ## Introduction
11 |
12 | First, thank you for considering contributing to AutoForm! It's people like you that make the open source community such a great community! 😊
13 |
14 | We welcome any type of contribution, not only code. You can help with
15 |
16 | - **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open or better yet pasted output from the console)
17 | - **Marketing**: writing blog posts, howto's, printing stickers, ...
18 | - **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, ...
19 | - **Code**: take a look at the [open issues](issues). Even if you can't write code, commenting on them, showing that you care about a given issue matters. It helps us triage them.
20 | - **Money**: we encourage financial contributions to individual developers through github sponsors. See the main page of this repo's **Sponsor this project** section
21 |
22 | ## Your First Contribution
23 |
24 | Working on your first Pull Request? You can learn how from this *free* series, [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
25 |
26 | ## Submitting code
27 |
28 | Any code change should be submitted as a pull request. The description should explain what the code does and give steps to execute it. The pull request should also contain tests.
29 |
30 | ## Code review process
31 |
32 | The bigger the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge.
33 | It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you?
34 |
35 | ## Financial contributions
36 |
37 | We encourage sponsorship of individual developers directly. If you'd like to contribute financially, please see the **Sponsor this project** section of [this repo](https://github.com/Meteor-Community-Packages/meteor-fast-render) to find a developer that helps make this project possible. Also don't forget to explore other [Meteor Community Packages](https://github.com/Meteor-Community-Packages) repositories and check their **Sponsor this project** sections for other amazing developers doing great work to keep these packages alive and well.
38 |
39 | ## Questions
40 |
41 | If you have any questions, create an [issue](issue) (**protip**: do a quick search first to see if someone else has already asked the same question before!).
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (c) 2014 Arunoda Susiripala
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | 'Software'), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fast Render
2 |
3 | Fast Render can improve the initial load time of your app, giving you 2-10 times faster initial page loads.
4 |
5 | > This is a continuation of `meteorhacks:fast-render` by @arunoda
6 |
7 | ## Table of Contents
8 |
9 | - [Table of Contents](#table-of-contents)
10 | - [Usage](#usage)
11 | - [How Fast Render Works](#how-fast-render-works)
12 | - [Client-side timing control](#client-side-timing-control)
13 | - [SSR API](#ssr-api)
14 | - [View layer specific SSR packages](#view-layer-specific-ssr-packages)
15 | - [What else can we do?](#what-else-can-we-do)
16 | - [Extra Data API](#extra-data-api)
17 | - [FastRender.addExtraData('key', data)](#fastrenderaddextradatakey-data)
18 | - [FastRender.getExtraData('key')](#fastrendergetextradatakey)
19 | - [Route API](#route-api)
20 | - [FastRender.route(callback)](#fastrenderroutecallback)
21 | - [FastRender.onAllRoutes(callback)](#fastrenderonallroutescallback)
22 | - [Security](#security)
23 | - [Side Effects](#side-effects)
24 | - [CORS Headers](#cors-headers)
25 | - [Cookie Tossing](#cookie-tossing)
26 | - [Known Issues](#known-issues)
27 | - [Client Error: "Server sent add for existing id"](#client-error-server-sent-add-for-existing-id)
28 | - [No data is injected when using "AppCache" package](#no-data-is-injected-when-using-appcache-package)
29 | - [No data is injected when using Meteor Subscription Cache](#no-data-is-injected-when-using-meteor-subscription-cache)
30 | - [No support for publication strategies](#no-support-for-publication-strategies)
31 | - [Debugging](#debugging)
32 | - [Block DDP](#block-ddp)
33 | - [Get Payload](#get-payload)
34 | - [Disable Fast Render](#disable-fast-render)
35 | - [Logs](#logs)
36 | - [Development](#development)
37 | - [Setup](#setup)
38 | - [Testing](#testing)
39 | - [Committing Changes](#committing-changes)
40 |
41 |
42 | ## Usage
43 |
44 | Add Fast Render to your Meteor app:
45 |
46 | ```sh
47 | meteor add communitypackages:fast-render
48 | ```
49 |
50 | After that, either make sure you've moved any code that calls `Meteor.subscribe` to shared code space (client and server), or use the SSR, Route, or Extra Data API's to make data available on the client immediately upon page load.
51 |
52 | ## How Fast Render Works
53 |
54 | Fast render runs on the server and gets the subscription data relavant to the page you are loading. Then it sends that data as part of the initial HTML of the Meteor app as shown below:
55 |
56 | 
57 |
58 | Then Fast Render parses and loads that data into Meteor collections. This makes your Meteor app code think the data connection has been made, and it renders the page right away.
59 |
60 | ## Client-side timing control
61 |
62 | If your app calls subscriptions before FastRender has loaded it's data, you may get errors such as `Expected to find a document not present for an add` when you page loads on the client. To avoid these errors when not using the SSR API, use the `FastRender.onDataReady` method.
63 |
64 | ```js
65 | FastRender.onDataReady(() => {
66 | // It is now safe to render your UI and make subscription calls
67 | });
68 | ```
69 |
70 | ## SSR API
71 |
72 | Fast Render comes with helpers for server-side rendering
73 |
74 | FastRender will track subscriptions and load their data after your initial HTML render has been sent. The data will be immediately available for hydrating on the client. Use `FastRender.onPageLoad` instead of Meteor's `server-render` `onPageLoad`. You can use `Meteor.subscribe` in your UI code and the data will automatically be appended to the HTML document.
75 |
76 | On the server:
77 |
78 | ```js
79 | FastRender.onPageLoad(sink => {
80 | sink.renderIntoElementById('app', renderToString())
81 | })
82 | ```
83 |
84 | On the client:
85 |
86 | ```js
87 | FastRender.onPageLoad(async sink => {
88 | const App = (await import('/imports/components/App/App')).default
89 | ReactDOM.hydrate(, document.getElementById('app'))
90 | })
91 | ```
92 |
93 | **Let's talk about hydration:** This is a great opportunity to make fast server-side rendered applications. Your HTML output can be rendered in a stream to the client, and the JS is only loaded and parsed once the HTML has been fully rendered. The data added by this method would not slow down the initial load time (when using streams). By injecting all of the necessary data after the HTML, the page can be rendered by the server and loaded on the client very quickly, and then the client can hydrate the DOM as soon as the JS payload loads, without then waiting for the data to load. Keep an eye on Meteor's support for `renderToNodeStream`.
94 |
95 | ### View layer specific SSR packages
96 |
97 | Sometimes you just want to plug things in and have them work without having to wire them up, and so the following is hopefully an ever growing list of packages that build upon this one to simplify the creation of server rendered app for specific view layers. If you create a package that uses this one to provide SSR for a specific view layer, open a PR and list it here.
98 |
99 | - [communitypackages:react-router-ssr](https://packosphere.com/communitypackages/react-router-ssr)
100 |
101 | ### What else can we do?
102 |
103 | - Critical CSS - We can inline the CSS used by components (css-modules, etc.) and defer loading of the main stylesheet
104 | - Support for dynamically loaded components (react-loadable, etc.)
105 |
106 | ## Extra Data API
107 |
108 | In some instances your app may need to fetch data through methods that don't use the pub/sub paradigm. For instance your UI loads data from an external API or through a Meteor method call. In this case FastRender doesn't have an automatic way get an insert this data into your UI, but you can use the following API to add the data to the initial page HTML and retrieve it on the client when the UI loads.
109 |
110 | ### FastRender.addExtraData('key', data)
111 |
112 | On the server, this method allows you to add arbitrary data to the client payload that will be available under the 'key' using `FastRender.getExtraData` after the page loads on the client.
113 |
114 | On the client this method is a NOOP and is provided strictly so that you don't have to use client/server checks to avoid errors.
115 |
116 | ### FastRender.getExtraData('key')
117 |
118 | On the client, this method returns the data that was added to the payload under the `key`. Once this has been called for a certain `key`, further calls for that same `key` will return `null`. This is to have server data available when the page first loads, but subsequently load fresh data. For instance when routing and you get data for the page from fast render initially, but the next time the user visits the route in the app without refreshing the page, fresh data should be fetched.
119 |
120 | On the server this method is a NOOP and is provided strictly so that you don't have to use client/server checks to avoid errors.
121 |
122 | ## Route API
123 |
124 | If you're doing some custom subscription handling, Fast Render won't be able to identify those subscriptions.
125 |
126 | If you want to use Fast Render in these cases, you'll need to map subscriptions manually to routes. It can be done using the following APIs:
127 |
128 | > The following APIs are available on the server only.
129 |
130 | ### FastRender.route(callback)
131 |
132 | This declares server side routes using a URL pattern similar to Iron Router's. The callback runs in a context very similar to Meteor and you can use any Meteor APIs inside it (it runs on a Fiber). Inside, you can subscribe to publications using `this.subscribe`.
133 |
134 | Use it like this:
135 |
136 | ```js
137 | FastRender.route('/leaderboard/:date', function(params) {
138 | this.subscribe('leaderboard', params.date)
139 | })
140 | ```
141 |
142 | ### FastRender.onAllRoutes(callback)
143 |
144 | This is very similar to `FastRender.route`, but lets you register a callback which will run on all routes.
145 |
146 | Use it like this:
147 |
148 | ```js
149 | FastRender.onAllRoutes(function(path) {
150 | this.subscribe('currentUser')
151 | })
152 | ```
153 |
154 | ## Security
155 |
156 | Fast Render has the ability to get data related to a user by detecting `loggedIn` status. It does this by sending the same loginToken used by the DDP connection using cookies.
157 |
158 | This is not inherently bad, but this might potentially cause some security issues. Those issues are described below along with possible countermeasures. Fortunately, Fast Render has features to prevent some of them.
159 |
160 | > These issues were raised by [Emily Stark](https://twitter.com/estark37) from the [meteor-core team](https://groups.google.com/forum/#!msg/meteor-talk/1Fg4rNk9JZM/ELX3672QsrEJ).
161 |
162 | ### Side Effects
163 |
164 | It is possible to send custom HTTP requests to routes handled by Fast Render either using an XHR request or a direct HTTP request.
165 |
166 | So if you are doing some DB write operations or saving something to the filesystem, the code sent will be executed. this could be bad if the HTTP request is an XHR request called by a malicious user. They wouldn't be able read anything, but they could cause side effects.
167 |
168 | It is wise to avoid side effects from following places:
169 |
170 | - publications
171 | - fastRender routes
172 |
173 | ### CORS Headers
174 |
175 | If your app adds [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) headers via connect handlers, there is a potential security issue.
176 |
177 | Fast Render detects CORS headers with conflicting routes and turns off fast rendering for those routes.
178 |
179 | It's okay to add CORS headers to custom server side routes, but if they conflict with the client side routes (which are handled by Fast Render), then there will be a security issue. It would allow malicious XHR requests from other domains to access loggedIn user's subscription data.
180 |
181 | ### Cookie Tossing
182 |
183 | If your app is available under a shared domain like `*.meteor.com` or `*.herokuapp.com`, there is a potential [security issue](https://groups.google.com/forum/#!topic/meteor-talk/Zhy1c6MdOH8).
184 |
185 | **We've made some [protection](https://groups.google.com/d/msg/meteor-talk/LTO2n5D1bxY/J5EnVpJo0rAJ) to this issue; so you can still use Fast Render.**
186 |
187 | If you host your app under `*.meteor.com` etc. but use a separate domain, then your app will not be vulnerable in this way.
188 |
189 | ## Known Issues
190 |
191 | ### Client Error: "Server sent add for existing id"
192 |
193 | If you are getting this issue, it seems like you are doing a database write operation inside a `Template.rendered` (Template.yourTemplate.rendered).
194 |
195 | To get around with this issue, rather than invoking a DB operation with MiniMongo, try to invoke a method call.
196 |
197 | Related Issue & Discussion:
198 |
199 | ### No data is injected when using "AppCache" package
200 |
201 | Currently FastRender does not support simultaneous usage with [appcache package](https://atmospherejs.com/meteor/appcache)
202 |
203 | Related Issue & Discussion:
204 |
205 | ### No data is injected when using Meteor Subscription Cache
206 |
207 | When using the subscache package (`ccorcos:subs-cache` or `blockrazor:subscache-c4`) the parameters passed to the subscription must the identical on both Fast Render and Subscache or no data will be injected.
208 |
209 | ### No support for publication strategies
210 |
211 | This package could affect the behavior of non-default [publication strategies](https://docs.meteor.com/api/pubsub.html#Publication-strategies).
212 |
213 | ## Debugging
214 |
215 | Sometimes, you need to test whether Fast Render is working or not. You can do this using the built-in debugger. The debugger works on the client and is safe to run it on a deployed app. It has a few useful features:
216 |
217 | ### Block DDP
218 |
219 | You can block the DDP connection and check whether the page was fast rendered or not. Once blocked, no DDP messages will be accepted. To block, apply following command in the browser console:
220 |
221 | ```js
222 | FastRender.debugger.blockDDP()
223 | ```
224 |
225 | You can unblock it with:
226 |
227 | ```js
228 | FastRender.debugger.unblockDDP()
229 | ```
230 |
231 | ### Get Payload
232 |
233 | With the following command you can inspect the data that comes on a Fast Render page load:
234 |
235 | ```js
236 | FastRender.debugger.getPayload()
237 | ```
238 |
239 | It will be in this format:
240 |
241 | ```js
242 | {
243 | // subscriptions processed
244 | subscriptions: {
245 | courses: true,
246 | leaderBoard: true
247 | },
248 |
249 | // data grouped by collection name
250 | data: {
251 | courses: [
252 | [...],
253 | ],
254 | users: [
255 | [...]
256 | ]
257 | }
258 | }
259 | ```
260 |
261 | > You can also apply `FastRender.debugger.getPayloadJSON()` to get the logs as a JSON string.
262 |
263 | ### Disable Fast Render
264 |
265 | You can also use a command to disable Fast Render:
266 |
267 | ```js
268 | FastRender.debugger.disableFR()
269 | ```
270 |
271 | Re-enable it with:
272 |
273 | ```js
274 | FastRender.debugger.enableFR()
275 | ```
276 |
277 | ### Logs
278 |
279 | Fast Render has robust logging.
280 |
281 | You can turn it on using `FastRender.debugger.showLogs()`.
282 |
283 | Hide them again using `FastRender.debugger.hideLogs()`.
284 |
285 | You can get all of the log messages by using `FastRender.debugger.getLogs()` and `FastRender.debugger.getLogsJSON()`.
286 |
287 | ## Development
288 |
289 | This repo contains various tooling setups to help you keep code well formatted with consistent style, as well as tests to help ensure that your code changes don't break functionality. The following information will help you get started.
290 |
291 | ### Setup
292 |
293 | 1. Clone the repository
294 |
295 | ```sh
296 | git clone https://github.com/Meteor-Community-Packages/meteor-fast-render.git
297 | ```
298 |
299 | ```sh
300 | cd meteor-fast-render
301 | ```
302 |
303 | 2. Install Dependencies
304 |
305 | ```sh
306 | npm install
307 | ```
308 |
309 | 3. Install Optional Editor Specific Extensions
310 | - Editor Config - automatically configure spacing and line breaks
311 | - ESLint - show realtime JavaScript linting errors in editor
312 | - Markdown Linter - show realtime Markdown linting errors in the editor (markdownlint by *David Anson* for VSCode)
313 | - Markdown TOC generator - automatically keep Table of Contents up to date in README. (Markdown All in One by *Yu Zhang* for VSCode is a great extension for this.)
314 |
315 | These extensions are optional but they will help youto have consistent whitespace when switching projects, to spot issues sooner, and keep the TOC automaticallu up to date when making changes to markdown files.
316 |
317 | ### Testing
318 |
319 | This repo contains tests to help reduce bugs and breakage. Before committing and submitting your changes, you should run the tests and make sure they pass. Follow these steps to run the tests for this repo.
320 |
321 | 1. From the project directory, move into the testApp directory
322 |
323 | ```sh
324 | cd testApp
325 | ```
326 |
327 | 2. Run Setup
328 |
329 | ```sh
330 | npm run setup
331 | ```
332 |
333 | 3. Run Tests
334 |
335 | ```sh
336 | npm test
337 | ```
338 |
339 | ```sh
340 | npm run test:watch
341 | ```
342 |
343 | ### Committing Changes
344 |
345 | Upon commit, this repo will run a pre-commit hook to lint your changes. Minor fixable linting errors will be automatically fixed before commit. If the errors are not fixable, a message will be dispalyed and you will need to fix the errors before commiting the changes.
346 |
--------------------------------------------------------------------------------
/lib/client/auth.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 |
3 | // getting tokens for the first time
4 | // Meteor calls Meteor._localStorage.setItem() on the boot
5 | // But we can do it ourselves also with this
6 | Meteor.startup(function () {
7 | resetToken();
8 | });
9 |
10 | // override Meteor._localStorage methods and resetToken accordingly
11 | const originalSetItem = Meteor._localStorage.setItem;
12 | Meteor._localStorage.setItem = function (key, value) {
13 | if (key === 'Meteor.loginToken') {
14 | Meteor.defer(resetToken);
15 | }
16 | originalSetItem.call(Meteor._localStorage, key, value);
17 | };
18 |
19 | const originalRemoveItem = Meteor._localStorage.removeItem;
20 | Meteor._localStorage.removeItem = function (key) {
21 | if (key === 'Meteor.loginToken') {
22 | Meteor.defer(resetToken);
23 | }
24 | originalRemoveItem.call(Meteor._localStorage, key);
25 | };
26 |
27 | function resetToken () {
28 | const loginToken = Meteor._localStorage.getItem('Meteor.loginToken');
29 | const loginTokenExpires = new Date(
30 | Meteor._localStorage.getItem('Meteor.loginTokenExpires'),
31 | );
32 |
33 | if (loginToken) {
34 | setToken(loginToken, loginTokenExpires);
35 | } else {
36 | setToken(null, -1);
37 | }
38 | }
39 |
40 | function setToken (loginToken, expires) {
41 | let cookieString = `meteor_login_token=${encodeURIComponent(loginToken ?? '')}`;
42 | let date;
43 |
44 | if (typeof expires === 'number') {
45 | date = new Date();
46 | date.setDate(date.getDate() + expires);
47 | } else {
48 | date = expires;
49 | }
50 | cookieString += `; expires=${date.toUTCString()}; path=/`;
51 |
52 | document.cookie = cookieString;
53 | }
54 |
--------------------------------------------------------------------------------
/lib/client/boot.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { FastRender } from './fast_render';
3 | import { InjectData } from 'meteor/communitypackages:inject-data';
4 |
5 | Meteor.startup(function () {
6 | InjectData.getData('fast-render-data', function (payload) {
7 | FastRender.init(payload);
8 | InjectData.getData('fast-render-extra-data', function (extraDataPayload) {
9 | FastRender._setExtraData(extraDataPayload);
10 | FastRender._setDataReady();
11 | });
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/lib/client/ddp_update.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { FastRender } from './fast_render';
3 | import { EJSON } from 'meteor/ejson';
4 | import { IDTools } from './id_tools';
5 |
6 | let reconnecting = false;
7 |
8 | const originalLivedataData = Meteor.connection._livedata_data;
9 | Meteor.connection._livedata_data = function (msg) {
10 | if (FastRender._blockDDP && !msg.frGen) {
11 | FastRender.debugger.log('blocking incoming ddp', msg);
12 | return;
13 | }
14 | // fast-render adds data manually while initializing
15 | // But when the server sends actual data via DDP, it also tries to add
16 | // Then we need to detect that and alter
17 | //
18 | // But we don't need to interfer with Meteor's simulation process
19 | // That's why we are checking for serverDocs and ignore manual handling
20 | //
21 | // We don't need this logic after our special handling reverted back to
22 | // original. But we can't detect when null publications completed or not
23 | // That's why we need keep this logic
24 | //
25 | // It's okay to ignore this logic after sometime, but not sure when exactly
26 |
27 | if (msg.msg === 'added') {
28 | const id = IDTools.idParse(msg.id);
29 | const serverDoc = this._getServerDoc(msg.collection, id);
30 |
31 | if (!reconnecting && !serverDoc) {
32 | const localCollection = this._mongo_livedata_collections[msg.collection];
33 | const pendingStoreUpdates = this._updatesForUnknownStores[msg.collection];
34 | if (localCollection) {
35 | const existingDoc = localCollection.findOne(id);
36 | if (existingDoc) {
37 | FastRender.debugger.log('re writing DDP for:', msg);
38 | FastRender._AddedToChanged(existingDoc, msg);
39 | }
40 | } else if (pendingStoreUpdates) {
41 | let mergedDoc = null;
42 | const existingDocs = pendingStoreUpdates.filter(function (doc) {
43 | return doc.id === msg.id;
44 | });
45 |
46 | existingDocs.forEach(function (cachedMsg) {
47 | mergedDoc = FastRender._ApplyDDP(mergedDoc, cachedMsg);
48 | });
49 |
50 | if (mergedDoc) {
51 | FastRender.debugger.log('re writing DDP for:', msg);
52 | FastRender._AddedToChanged(mergedDoc, msg);
53 | }
54 | }
55 | }
56 | }
57 |
58 | // if we've completed our tasks, no need of special handling
59 | if (!FastRender._revertedBackToOriginal && FastRender._dataReceived) {
60 | // This will take care of cleaning special subscription handling
61 | // after the actual subscription comes out
62 | if (msg.msg === 'ready' && !msg.frGen && FastRender._subscriptions) {
63 | msg.subs.forEach(function (subId) {
64 | const subscription = FastRender._subscriptionIdMap[subId];
65 | if (subscription) {
66 | FastRender.debugger.log(
67 | 'actual subscription completed:',
68 | subscription,
69 | subId,
70 | );
71 | // we don't need to handle specially after this
72 | const paramsKeyMap = FastRender._subscriptions[subscription.name] || {};
73 | delete paramsKeyMap[subscription.paramsKey];
74 | if (EJSON.equals(FastRender._subscriptions[subscription.name], {})) {
75 | delete FastRender._subscriptions[subscription.name];
76 | }
77 | delete FastRender._subscriptionIdMap[subId];
78 | }
79 | });
80 | }
81 |
82 | // if all the subscriptions have been processed,
83 | // there is no need to keep hijacking
84 | if (EJSON.equals(FastRender._subscriptions, {})) {
85 | FastRender.debugger.log('fast rendering completed!');
86 | FastRender._revertedBackToOriginal = true;
87 | }
88 | }
89 |
90 | return originalLivedataData.call(this, msg);
91 | };
92 |
93 | const originalSend = Meteor.connection._send;
94 | Meteor.connection._send = function (msg) {
95 | // if looking for connect again to the server, we must need to revert back to
96 | // original to prevent some weird DDP issues
97 | // normally it is already reverted, but user may added subscriptions
98 | // in server, which are not subscribed from the client
99 | if (msg.msg === 'connect' && msg.session !== undefined) {
100 | FastRender._revertedBackToOriginal = true;
101 | reconnecting = true;
102 | }
103 |
104 | // if we've completed our tasks, no need of special handling
105 | if (!FastRender._revertedBackToOriginal && FastRender._dataReceived) {
106 | const paramsKey = EJSON.stringify(msg.params);
107 | const canSendFakeReady =
108 | msg.msg === 'sub' &&
109 | FastRender._subscriptions[msg.name] &&
110 | FastRender._subscriptions[msg.name][paramsKey];
111 |
112 | FastRender.debugger.log('new subscription:', msg.name);
113 | if (canSendFakeReady) {
114 | FastRender.debugger.log('sending fake ready for sub:', msg.name);
115 | FastRender.injectDdpMessage(this, {
116 | msg: 'ready',
117 | subs: [msg.id],
118 | frGen: true,
119 | });
120 | // add the messageId to be handled later
121 | FastRender._subscriptionIdMap[msg.id] = {
122 | name: msg.name,
123 | paramsKey: paramsKey,
124 | };
125 | }
126 | }
127 |
128 | return originalSend.call(this, msg);
129 | };
130 |
--------------------------------------------------------------------------------
/lib/client/fast_render.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { Accounts } from 'meteor/accounts-base';
3 | import { onPageLoad } from 'meteor/server-render';
4 | import { IDTools } from './id_tools';
5 |
6 | import './auth';
7 | import './boot';
8 | import './ddp_update';
9 |
10 | let extraneousData = {};
11 |
12 | export const FastRender = {
13 | _dataReceived: false,
14 | _dataReadyCallback: null,
15 | _dataReady: false,
16 | _revertedBackToOriginal: false,
17 | IDTools,
18 | _blockDDP: Meteor._localStorage.getItem('__frblockddp') !== null,
19 | _disable: Meteor._localStorage.getItem('__frdisable') !== null,
20 | debugger: {
21 | _logs: [],
22 |
23 | log (message /*, args.. */) {
24 | if (
25 | typeof console !== 'undefined' &&
26 | typeof Meteor._localStorage !== 'undefined' &&
27 | Meteor._localStorage.getItem('__frlog') === '1'
28 | ) {
29 | FastRender.debugger._logs.push(arguments);
30 | arguments[0] = 'FR: ' + arguments[0];
31 | console.log.apply(console, arguments);
32 | }
33 | },
34 |
35 | showLogs () {
36 | Meteor._localStorage.setItem('__frlog', '1');
37 | location.reload();
38 | },
39 |
40 | hideLogs () {
41 | Meteor._localStorage.removeItem('__frlog');
42 | location.reload();
43 | },
44 |
45 | getLogs () {
46 | return FastRender.debugger._logs;
47 | },
48 |
49 | getLogsJSON () {
50 | return JSON.stringify(FastRender.debugger._logs);
51 | },
52 |
53 | blockDDP () {
54 | Meteor._localStorage.setItem('__frblockddp', '1');
55 | location.reload();
56 | },
57 |
58 | unblockDDP () {
59 | Meteor._localStorage.removeItem('__frblockddp');
60 | location.reload();
61 | },
62 |
63 | disableFR () {
64 | Meteor._localStorage.setItem('__frdisable', '1');
65 | location.reload();
66 | },
67 |
68 | enableFR () {
69 | Meteor._localStorage.removeItem('__frdisable');
70 | location.reload();
71 | },
72 |
73 | getPayload () {
74 | return FastRender._payload;
75 | },
76 |
77 | getPayloadJSON () {
78 | return JSON.stringify(FastRender._payload);
79 | },
80 | },
81 |
82 | // This allow us to apply DDP message even if Meteor block accepting messages
83 | // When doing initial login, Meteor sends an login message
84 | // Then it'll block the accpeting DDP messages from server
85 | // This is the cure
86 | injectDdpMessage (conn, message) {
87 | FastRender.debugger.log('injecting ddp message:', message);
88 |
89 | // Removed check for conn._bufferedWrites due to https://github.com/kadirahq/fast-render/pull/167/files#r74189260
90 | // and https://github.com/kadirahq/fast-render/issues/176
91 |
92 | const originalWait = conn._waitingForQuiescence;
93 | conn._waitingForQuiescence = function () {
94 | return false;
95 | };
96 | conn._livedata_data(message);
97 | conn._waitingForQuiescence = originalWait;
98 | },
99 |
100 | _setExtraData (data) {
101 | extraneousData = data;
102 | },
103 |
104 | addExtraData () {
105 | // we provide this method for symmetry to avoid having to use isClient/isServer checks
106 | },
107 |
108 | getExtraData (key) {
109 | const data = extraneousData[key];
110 | if (data) {
111 | delete extraneousData[key];
112 | return data;
113 | } else {
114 | return null;
115 | }
116 | },
117 |
118 | init (payload) {
119 | if (FastRender._disable) return;
120 |
121 | FastRender._securityCheck(payload);
122 | FastRender._subscriptions = (payload && payload.subscriptions) || {};
123 | FastRender._subscriptionIdMap = {};
124 | FastRender._dataReceived = true;
125 | FastRender._payload = payload;
126 |
127 | // merging data from different subscriptions
128 | // yes, this is a minimal mergeBox on the client
129 | const allData = {};
130 | if (payload) {
131 | Object.keys(payload.collectionData).forEach(function (collName) {
132 | const subData = payload.collectionData[collName];
133 | if (!allData[collName]) {
134 | allData[collName] = {};
135 | }
136 | const collData = allData[collName];
137 |
138 | subData.forEach(function (dataSet) {
139 | dataSet.forEach(function (item) {
140 | if (!collData[item._id]) {
141 | collData[item._id] = item;
142 | } else {
143 | FastRender._DeepExtend(collData[item._id], item);
144 | }
145 | });
146 | });
147 | });
148 | }
149 |
150 | const connection = Meteor.connection;
151 |
152 | Object.keys(allData).forEach(function (collName) {
153 | const collData = allData[collName];
154 | Object.keys(collData).forEach(function (id) {
155 | const item = collData[id];
156 | id = IDTools.idStringify(item._id);
157 | delete item._id;
158 |
159 | const ddpMessage = {
160 | msg: 'added',
161 | collection: collName,
162 | id: id,
163 | fields: item,
164 | frGen: true,
165 | };
166 |
167 | FastRender.injectDdpMessage(connection, ddpMessage);
168 | try {
169 | // If the connection supports buffered DDP writes, then flush now.
170 | if (connection._flushBufferedWrites) connection._flushBufferedWrites();
171 | } catch (e) {
172 | console.error(
173 | 'FastRender was unable to simulate the following DDP message: ',
174 | ddpMessage,
175 | e,
176 | );
177 | }
178 | });
179 | });
180 |
181 | // let Meteor know, user login process has been completed
182 | if (typeof Accounts !== 'undefined') {
183 | Accounts._setLoggingIn(false);
184 | }
185 | },
186 |
187 | _securityCheck (payload) {
188 | if (payload && payload.loginToken) {
189 | const localStorageLoginToken = Meteor._localStorage.getItem(
190 | 'Meteor.loginToken',
191 | );
192 | if (localStorageLoginToken !== payload.loginToken) {
193 | throw new Error(
194 | 'seems like cookie tossing is happening. visit here: http://git.io/q4IRHQ',
195 | );
196 | }
197 | }
198 | },
199 |
200 | _AddedToChanged (localCopy, added) {
201 | added.msg = 'changed';
202 | added.cleared = [];
203 | added.fields = added.fields || {};
204 |
205 | Object.keys(localCopy).forEach(function (key) {
206 | if (key !== '_id') {
207 | if (typeof added.fields[key] === 'undefined') {
208 | added.cleared.push(key);
209 | }
210 | }
211 | });
212 | },
213 |
214 | _ApplyDDP (existing, message) {
215 | let newDoc = !existing ? {} : Object.assign({}, existing);
216 | if (message.msg === 'added') {
217 | Object.keys(message.fields).forEach(function (key) {
218 | newDoc[key] = message.fields[key];
219 | });
220 | } else if (message.msg === 'changed') {
221 | Object.keys(message.fields).forEach(function (key) {
222 | newDoc[key] = message.fields[key];
223 | });
224 | message.cleared.forEach(function (key) {
225 | delete newDoc[key];
226 | });
227 | } else if (message.msg === 'removed') {
228 | newDoc = null;
229 | }
230 |
231 | return newDoc;
232 | },
233 |
234 | // source: https://gist.github.com/kurtmilam/1868955
235 | // modified a bit to not to expose this as an _ api
236 | _DeepExtend (obj) {
237 | const parentRE = /#{\s*?_\s*?}/;
238 | const slice = Array.prototype.slice;
239 | const hasOwnProperty = Object.prototype.hasOwnProperty;
240 | slice.call(arguments, 1).forEach(function (source) {
241 | for (const prop in source) {
242 | if (hasOwnProperty.call(source, prop)) {
243 | if (
244 | obj[prop] === null ||
245 | obj[prop] === undefined ||
246 | typeof obj[prop] === 'function' ||
247 | source[prop] === null ||
248 | source[prop] instanceof Date
249 | ) {
250 | obj[prop] = source[prop];
251 | } else if (typeof source[prop] === 'string' && parentRE.test(source[prop])) {
252 | if (typeof obj[prop] === 'string') {
253 | obj[prop] = source[prop].replace(parentRE, obj[prop]);
254 | }
255 | } else if (obj[prop] instanceof Array || source[prop] instanceof Array) {
256 | if (!(obj[prop] instanceof Array) || !(source[prop] instanceof Array)) {
257 | throw new Meteor.Error('Error: Trying to combine an array with a non-array (' +
258 | prop +
259 | ')');
260 | } else {
261 | obj[prop] = FastRender._DeepExtend(obj[prop], source[prop]).filter(
262 | function (item) {
263 | return item !== null;
264 | },
265 | );
266 | }
267 | } else if (typeof obj[prop] === 'object' || typeof source[prop] === 'object') {
268 | if (typeof obj[prop] !== 'object' || typeof source[prop] !== 'object') {
269 | throw new Meteor.Error('Error: Trying to combine an object with a non-object (' +
270 | prop +
271 | ')');
272 | } else {
273 | obj[prop] = FastRender._DeepExtend(obj[prop], source[prop]);
274 | }
275 | } else {
276 | obj[prop] = source[prop];
277 | }
278 | }
279 | }
280 | });
281 | return obj;
282 | },
283 |
284 | onDataReady (callback) {
285 | FastRender._runIfDataReady(callback);
286 | },
287 |
288 | onPageLoad (callback) {
289 | onPageLoad(sink => {
290 | FastRender._runIfDataReady(() => callback(sink));
291 | });
292 | },
293 |
294 | _setDataReady () {
295 | if (FastRender._dataReadyCallback !== null && typeof FastRender._dataReadyCallback === 'function') {
296 | FastRender._dataReadyCallback();
297 | } else {
298 | FastRender._dataReady = true;
299 | }
300 | },
301 |
302 | _runIfDataReady (callback) {
303 | if (FastRender._dataReady) {
304 | callback();
305 | } else {
306 | FastRender._dataReadyCallback = callback;
307 | }
308 | },
309 | };
310 |
311 | if (FastRender._blockDDP) {
312 | console.log(
313 | "FastRender is blocking DDP messages. apply 'FastRender.debugger.unblockDDP()' to unblock again.",
314 | );
315 | }
316 |
317 | if (FastRender._disable) {
318 | console.log(
319 | "FastRender is disabled. apply 'FastRender.debugger.enableFR()' to enable it back.",
320 | );
321 | }
322 |
--------------------------------------------------------------------------------
/lib/client/id_tools.js:
--------------------------------------------------------------------------------
1 | import { MongoID } from 'meteor/mongo-id';
2 |
3 | export const IDTools = {
4 | idParse: MongoID.idParse,
5 | idStringify: MongoID.idStringify,
6 | ObjectID: MongoID.ObjectID,
7 | };
8 |
--------------------------------------------------------------------------------
/lib/server/context.js:
--------------------------------------------------------------------------------
1 | import { Meteor } from 'meteor/meteor';
2 | import { Log } from 'meteor/logging';
3 | import Fibers from 'fibers';
4 | import Future from 'fibers/future';
5 | import { Accounts } from 'meteor/accounts-base';
6 | import { DDP } from 'meteor/ddp';
7 | import { Random } from 'meteor/random';
8 | import PublishContext from './publish_context';
9 | import { EJSON } from 'meteor/ejson';
10 |
11 | export class Context {
12 | constructor (loginToken, otherParams) {
13 | this._collectionData = {};
14 | this._subscriptions = {};
15 | this._extraData = {};
16 | this._loginToken = loginToken;
17 |
18 | Object.assign(this, otherParams);
19 |
20 | // get the user
21 | if (Meteor.users) {
22 | let user;
23 | // check to make sure, we've the loginToken,
24 | // otherwise a random user will fetched from the db
25 | if (loginToken) {
26 | const hashedToken = loginToken && Accounts._hashLoginToken(loginToken);
27 | const query = { 'services.resume.loginTokens.hashedToken': hashedToken };
28 | const options = { fields: { _id: 1 } };
29 | user = Meteor.users.findOne(query, options);
30 | }
31 |
32 | // support for Meteor.user
33 | Fibers.current._meteor_dynamics = [];
34 | Fibers.current._meteor_dynamics[DDP._CurrentInvocation.slot] = this;
35 |
36 | if (user) {
37 | this.userId = user._id;
38 | }
39 | }
40 | }
41 |
42 | subscribe (subName /*, params */) {
43 | const publishHandler = Meteor.server.publish_handlers[subName];
44 | if (publishHandler) {
45 | const params = Array.prototype.slice.call(arguments, 1);
46 | // non-universal subs have subscription id
47 | const subscriptionId = Random.id();
48 | const publishContext = new PublishContext(
49 | this,
50 | publishHandler,
51 | subscriptionId,
52 | params,
53 | subName,
54 | );
55 |
56 | return this.processPublication(publishContext);
57 | } else {
58 | Log.warn('There is no such publish handler named:', subName);
59 | return {};
60 | }
61 | }
62 |
63 | processPublication (publishContext) {
64 | const data = {};
65 | const ensureCollection = (collectionName) => {
66 | this._ensureCollection(collectionName);
67 | if (!data[collectionName]) {
68 | data[collectionName] = [];
69 | }
70 | };
71 |
72 | const future = new Future();
73 | // detect when the context is ready to be sent to the client
74 | publishContext.onStop(function () {
75 | if (!future.isResolved()) {
76 | future.return();
77 | }
78 | });
79 |
80 | publishContext._runHandler();
81 |
82 | if (!publishContext._subscriptionId) {
83 | // universal subscription, we stop it (same as marking it as ready) ourselves
84 | // they otherwise do not have ready or stopped state, but in our case they do
85 | publishContext.stop();
86 | }
87 |
88 | if (!future.isResolved()) {
89 | // don't wait forever for handler to fire ready()
90 | Meteor.setTimeout(() => {
91 | if (!future.isResolved()) {
92 | // publish handler failed to send ready signal in time
93 | // maybe your non-universal publish handler is not calling this.ready()?
94 | // or maybe it is returning null to signal empty publish?
95 | // it should still call this.ready() or return an empty array []
96 | const message =
97 | 'Publish handler for ' +
98 | publishContext._name +
99 | ' sent no ready signal\n' +
100 | ' This could be because this publication `return null`.\n' +
101 | ' Use `return this.ready()` instead.';
102 | Log.warn(message);
103 | future.return();
104 | }
105 | }, 500); // arbitrarially set timeout to 500ms, should probably be configurable
106 |
107 | // wait for the subscription became ready.
108 | future.wait();
109 | }
110 |
111 | // stop any runaway subscription
112 | // this can happen if a publish handler never calls ready or stop, for example
113 | // it does not hurt to call it multiple times
114 | publishContext.stop();
115 |
116 | // get the data
117 | for (let [collectionName, collData] of Object.entries(publishContext._collectionData)) {
118 | // making an array from a map
119 | collData = Object.values(collData);
120 |
121 | ensureCollection(collectionName);
122 | data[collectionName].push(collData);
123 |
124 | // copy the collection data in publish context into the FR context
125 | this._collectionData[collectionName].push(collData);
126 | }
127 |
128 | return data;
129 | }
130 |
131 | completeSubscriptions (name, params) {
132 | let subs = this._subscriptions[name];
133 | if (!subs) {
134 | subs = this._subscriptions[name] = {};
135 | }
136 |
137 | if (params && params.length) {
138 | const lastParam = params[params.length - 1];
139 | if (
140 | lastParam &&
141 | (Object.prototype.hasOwnProperty.call(lastParam, 'onStop') ||
142 | Object.prototype.hasOwnProperty.call(lastParam, 'onReady'))
143 | ) {
144 | params.pop();
145 | }
146 | }
147 |
148 | subs[EJSON.stringify(params)] = true;
149 | }
150 |
151 | _ensureCollection (collectionName) {
152 | if (!this._collectionData[collectionName]) {
153 | this._collectionData[collectionName] = [];
154 | }
155 | }
156 |
157 | getData () {
158 | return {
159 | collectionData: this._collectionData,
160 | subscriptions: this._subscriptions,
161 | loginToken: this._loginToken,
162 | };
163 | }
164 |
165 | addExtraData (key, data) {
166 | this._extraData[key] = data;
167 | }
168 |
169 | getExtraData () {
170 | return this._extraData;
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/lib/server/fast_render.js:
--------------------------------------------------------------------------------
1 | import Fiber from 'fibers';
2 | import { Meteor } from 'meteor/meteor';
3 | import { InjectData } from 'meteor/communitypackages:inject-data';
4 | import { onPageLoad } from 'meteor/server-render';
5 | import PublishContext from './publish_context';
6 | import { Context } from './context';
7 | import { setQueryDataCallback, handleError } from './utils';
8 | import { fastRenderRoutes } from './routes';
9 |
10 | const originalSubscribe = Meteor.subscribe;
11 | Meteor.subscribe = function (name, ...args) {
12 | const frContext = FastRender.frContext.get();
13 | if (!frContext) {
14 | throw new Error(
15 | `Cannot add a subscription: ${name} without FastRender Context`,
16 | );
17 | }
18 | frContext.subscribe(name, ...args);
19 |
20 | if (originalSubscribe) {
21 | originalSubscribe.apply(this, arguments);
22 | }
23 |
24 | return {
25 | ready: () => true,
26 | };
27 | };
28 |
29 | export const FastRender = {
30 | _routes: [],
31 | _onAllRoutes: [],
32 | _Context: Context,
33 | frContext: new Meteor.EnvironmentVariable(),
34 |
35 | // handling specific routes
36 | route (path, callback) {
37 | if (path.indexOf('/') !== 0) {
38 | throw new Error(
39 | 'Error: path (' + path + ') must begin with a leading slash "/"',
40 | );
41 | }
42 | fastRenderRoutes.route(path, FastRender.handleRoute.bind(null, callback));
43 | },
44 |
45 | handleRoute (processingCallback, params, req, res, next) {
46 | const afterProcessed = setQueryDataCallback(req, next);
47 | FastRender._processRoutes(params, req, processingCallback, afterProcessed);
48 | },
49 |
50 | handleOnAllRoutes (req, res, next) {
51 | const afterProcessed = setQueryDataCallback(req, next);
52 | FastRender._processAllRoutes(req, afterProcessed);
53 | },
54 |
55 | onAllRoutes (callback) {
56 | FastRender._onAllRoutes.push(callback);
57 | },
58 |
59 | _processRoutes (
60 | params,
61 | req,
62 | routeCallback,
63 | callback,
64 | ) {
65 | callback = callback || function () { };
66 |
67 | const path = req.url;
68 | const loginToken = req.cookies.meteor_login_token;
69 | const headers = req.headers;
70 |
71 | const context = new FastRender._Context(loginToken, { headers: headers });
72 |
73 | try {
74 | FastRender.frContext.withValue(context, function () {
75 | routeCallback.call(context, params, path);
76 | });
77 |
78 | if (context.stop) {
79 | return;
80 | }
81 |
82 | callback(context.getData());
83 | } catch (err) {
84 | handleError(err, path, callback);
85 | }
86 | },
87 |
88 | _processAllRoutes (req, callback) {
89 | callback = callback || function () { };
90 |
91 | const path = req.url;
92 | const loginToken = req.cookies.meteor_login_token;
93 | const headers = req.headers;
94 |
95 | new Fiber(function () {
96 | const context = new FastRender._Context(loginToken, { headers: headers });
97 |
98 | try {
99 | FastRender._onAllRoutes.forEach(function (callback) {
100 | callback.call(context, req.url);
101 | });
102 |
103 | callback(context.getData());
104 | } catch (err) {
105 | handleError(err, path, callback);
106 | }
107 | }).run();
108 | },
109 |
110 | _mergeFrData (req, queryData, extraData) {
111 | const existingQueryData = InjectData.getData(req, 'fast-render-data');
112 | let existingExtraData = InjectData.getData(req, 'fast-render-extra-data');
113 | if (!existingQueryData) {
114 | InjectData.pushData(req, 'fast-render-data', queryData);
115 | } else {
116 | // it's possible to execute this callback twice
117 | // the we need to merge exisitng data with the new one
118 | existingQueryData.subscriptions = { ...existingQueryData.subscriptions, ...queryData.subscriptions };
119 | for (let [pubName, data] of Object.entries(queryData.collectionData)) {
120 | const existingData = existingQueryData.collectionData[pubName];
121 | if (existingData) {
122 | data = existingData.concat(data);
123 | }
124 |
125 | existingQueryData.collectionData[pubName] = data;
126 | InjectData.pushData(req, 'fast-render-data', existingQueryData);
127 | }
128 | }
129 |
130 | if (!existingExtraData) {
131 | InjectData.pushData(req, 'fast-render-extra-data', extraData);
132 | } else {
133 | existingExtraData = { ...existingExtraData, ...extraData };
134 | InjectData.pushData(req, 'fast-render-extra-data', existingExtraData);
135 | }
136 | },
137 |
138 | onPageLoad (callback) {
139 | InjectData.injectToHead = false;
140 | onPageLoad(async sink => {
141 | const frContext = new FastRender._Context(
142 | sink.request.cookies.meteor_login_token,
143 | {
144 | headers: sink.headers,
145 | },
146 | );
147 |
148 | await FastRender.frContext.withValue(frContext, async function () {
149 | await callback(sink);
150 | const context = FastRender.frContext.get();
151 | const data = context.getData();
152 | const extraData = context.getExtraData();
153 | FastRender._mergeFrData(
154 | sink.request,
155 | data,
156 | extraData,
157 | );
158 | });
159 | });
160 | },
161 |
162 | addExtraData (key, data) {
163 | const frContext = FastRender.frContext.get();
164 | if (!frContext) {
165 | throw new Error(
166 | `Cannot add extra data: ${key} without FastRender Context`,
167 | );
168 | }
169 | frContext.addExtraData(key, data);
170 | },
171 |
172 | getExtraData () {
173 | // we provide this method for symmetry to avoid having to use isClient/isServer checks
174 | },
175 | };
176 |
177 | // adding support for null publications
178 | FastRender.onAllRoutes(function () {
179 | const context = this;
180 | const nullHandlers = Meteor.server.universal_publish_handlers;
181 |
182 | if (nullHandlers) {
183 | nullHandlers.forEach(function (publishHandler) {
184 | // universal subs have subscription ID, params, and name undefined
185 | const publishContext = new PublishContext(context, publishHandler);
186 | context.processPublication(publishContext);
187 | });
188 | }
189 | });
190 |
--------------------------------------------------------------------------------
/lib/server/publish_context.js:
--------------------------------------------------------------------------------
1 | import { Random } from 'meteor/random';
2 | import { Log } from 'meteor/logging';
3 | import { EJSON } from 'meteor/ejson';
4 | import { Meteor } from 'meteor/meteor';
5 | import { MeteorX } from 'meteor/montiapm:meteorx';
6 |
7 | // mock server
8 | const server = {
9 | getPublicationStrategy () {
10 | return {
11 | useCollectionView: true,
12 | doAccountingForCollection: true,
13 | };
14 | },
15 | };
16 |
17 | const PublishContext = function PublishContext (
18 | context,
19 | handler,
20 | subscriptionId,
21 | params,
22 | name,
23 | ) {
24 | // mock session
25 | const sessionId = Random.id();
26 | const session = {
27 | server,
28 | id: sessionId,
29 | userId: context.userId,
30 | // not null
31 | inQueue: {},
32 | connectionHandle: {
33 | id: sessionId,
34 | close: function () { },
35 | onClose: function () { },
36 | clientAddress: '127.0.0.1',
37 | httpHeaders: context.headers,
38 | },
39 | added: (subscriptionHandle, collectionName, strId, fields) => {
40 | // Don't share state with the data passed in by the user.
41 | const doc = EJSON.clone(fields);
42 | doc._id = this._idFilter.idParse(strId);
43 | Meteor._ensure(this._collectionData, collectionName)[strId] = doc;
44 | },
45 | changed: (subscriptionHandle, collectionName, strId, fields) => {
46 | const doc = this._collectionData[collectionName][strId];
47 | if (!doc) {
48 | throw new Error(
49 | 'Could not find element with id ' + strId + ' to change',
50 | );
51 | }
52 | for (const [key, value] of Object.entries(fields)) {
53 | // Publish API ignores _id if present in fields.
54 | if (key === '_id') return;
55 |
56 | if (value === undefined) {
57 | delete doc[key];
58 | } else {
59 | // Don't share state with the data passed in by the user.
60 | doc[key] = EJSON.clone(value);
61 | }
62 | }
63 | },
64 | removed: (subscriptionHandle, collectionName, strId) => {
65 | if (
66 | !(
67 | this._collectionData[collectionName] &&
68 | this._collectionData[collectionName][strId]
69 | )
70 | ) {
71 | throw new Error('Removed nonexistent document ' + strId);
72 | }
73 | delete this._collectionData[collectionName][strId];
74 | },
75 | sendReady: (subscriptionIds) => {
76 | // this is called only for non-universal subscriptions
77 | if (!this._subscriptionId) throw new Error('Assertion.');
78 |
79 | // make the subscription be marked as ready
80 | if (!this._isDeactivated()) {
81 | this._context.completeSubscriptions(this._name, this._params);
82 | }
83 |
84 | // we just stop it
85 | this.stop();
86 | },
87 | };
88 |
89 | MeteorX.Subscription.call(
90 | this,
91 | session,
92 | handler,
93 | subscriptionId,
94 | params,
95 | name,
96 | );
97 |
98 | this.unblock = function () { };
99 |
100 | this._context = context;
101 | this._collectionData = {};
102 | };
103 |
104 | PublishContext.prototype = Object.create(MeteorX.Subscription.prototype);
105 | PublishContext.prototype.constructor = PublishContext;
106 |
107 | PublishContext.prototype.stop = function () {
108 | // our stop does not remove all documents (it just calls deactivate)
109 | // Meteor one removes documents for non-universal subscription
110 | // we deactivate both for universal and named subscriptions
111 | // hopefully this is right in our case
112 | // Meteor does it just for named subscriptions
113 | this._deactivate();
114 | };
115 |
116 | PublishContext.prototype.error = function (error) {
117 | // TODO: Should we pass the error to the subscription somehow?
118 | Log.warn(
119 | 'error caught on publication: ',
120 | this._name,
121 | ': ',
122 | error.message || error,
123 | );
124 | this.stop();
125 | };
126 |
127 | export default PublishContext;
128 |
--------------------------------------------------------------------------------
/lib/server/routes.js:
--------------------------------------------------------------------------------
1 | import { FastRender } from './fast_render';
2 | import { Picker } from 'meteor/communitypackages:picker';
3 | import { IsAppUrl } from './utils';
4 | import cookieParser from 'cookie-parser';
5 |
6 | export const fastRenderRoutes = Picker.filter(function (req, res) {
7 | return IsAppUrl(req);
8 | });
9 |
10 | fastRenderRoutes.middleware(cookieParser());
11 | fastRenderRoutes.route('(.*)', function (params, req, res, next) {
12 | FastRender.handleOnAllRoutes(req, res, next);
13 | });
14 |
--------------------------------------------------------------------------------
/lib/server/utils.js:
--------------------------------------------------------------------------------
1 | import { RoutePolicy } from 'meteor/routepolicy';
2 | import { Log } from 'meteor/logging';
3 | import { InjectData } from 'meteor/communitypackages:inject-data';
4 |
5 | // meteor algorithm to check if this is a meteor serving http request or not
6 | export const IsAppUrl = (req) => {
7 | const url = req.url;
8 | if (url === '/favicon.ico' || url === '/robots.txt') {
9 | return false;
10 | }
11 |
12 | // NOTE: app.manifest is not a web standard like favicon.ico and
13 | // robots.txt. It is a file name we have chosen to use for HTML5
14 | // appcache URLs. It is included here to prevent using an appcache
15 | // then removing it from poisoning an app permanently. Eventually,
16 | // once we have server side routing, this won't be needed as
17 | // unknown URLs with return a 404 automatically.
18 | if (url === '/app.manifest') {
19 | return false;
20 | }
21 |
22 | // Avoid serving app HTML for declared routes such as /sockjs/.
23 | if (RoutePolicy.classify(url)) {
24 | return false;
25 | }
26 |
27 | // we only need to support HTML pages only
28 | // this is a check to do it
29 | return /html/.test(req.headers.accept);
30 | };
31 |
32 | export const setQueryDataCallback = (req, next) => {
33 | return function (queryData) {
34 | if (!queryData) return next();
35 |
36 | const existingPayload = InjectData.getData(req, 'fast-render-data');
37 | if (!existingPayload) {
38 | InjectData.pushData(req, 'fast-render-data', queryData);
39 | } else {
40 | // it's possible to execute this callback twice
41 | // the we need to merge exisitng data with the new one
42 | existingPayload.subscriptions = { ...existingPayload.subscriptions, ...queryData.subscriptions };
43 | for (let [pubName, data] of Object.entries(queryData.collectionData)) {
44 | const existingData = existingPayload.collectionData[pubName];
45 | if (existingData) {
46 | data = existingData.concat(data);
47 | }
48 |
49 | existingPayload.collectionData[pubName] = data;
50 | InjectData.pushData(req, 'fast-render-data', existingPayload);
51 | }
52 | }
53 | next();
54 | };
55 | };
56 |
57 | export const handleError = (err, path, callback) => {
58 | const message = 'error on fast-rendering path: ' + path + ' ; error: ' + err.stack;
59 | Log.error(message);
60 | callback(null);
61 | };
62 |
--------------------------------------------------------------------------------
/package.js:
--------------------------------------------------------------------------------
1 | /* global Package Npm */
2 | Package.describe({
3 | summary: 'Render your app before the DDP connection even comes alive - magic?',
4 | version: '4.0.9',
5 | git: 'https://github.com/Meteor-Community-Packages/meteor-fast-render',
6 | name: 'communitypackages:fast-render',
7 | });
8 |
9 | Npm.depends({
10 | 'cookie-parser': '1.4.6',
11 | });
12 |
13 | Package.onUse(function (api) {
14 | api.versionsFrom(['1.6.1', '2.3', '3.0-rc.1']);
15 |
16 | api.use(['communitypackages:picker@1.2.0', 'montiapm:meteorx@2.3.1'], 'server');
17 | api.use('communitypackages:inject-data@2.3.3');
18 | api.use(['livedata', 'webapp', 'routepolicy', 'random', 'logging'], 'server');
19 | api.use(['ecmascript', 'server-render', 'accounts-base', 'ejson', 'minimongo']);
20 |
21 | api.mainModule('lib/client/fast_render.js', 'client');
22 | api.mainModule('lib/server/fast_render.js', 'server');
23 | });
24 |
25 | Package.onTest(function (api) {
26 | api.use(['meteortesting:browser-tests', 'meteortesting:mocha']);
27 | api.use(['ecmascript', 'random', 'mongo', 'server-render', 'communitypackages:fast-render']);
28 | api.use('http', 'server');
29 |
30 | api.mainModule('tests/client/index.js', 'client');
31 | api.mainModule('tests/server/index.js', 'server');
32 | });
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "meteor-fast-render",
3 | "version": "4.0.9",
4 | "scripts": {
5 | "lint": "eslint ./ && markdownlint ./",
6 | "publish-release": "rimraf ./node_modules && meteor publish && npm i --only=dev",
7 | "pre-commit": "lint-staged",
8 | "prepare": "husky install"
9 | },
10 | "devDependencies": {
11 | "@babel/core": "7.24.5",
12 | "@babel/eslint-parser": "^7.24.5",
13 | "eslint": "^7.32.0",
14 | "eslint-config-standard": "^16.0.3",
15 | "eslint-import-resolver-meteor": "^0.4.0",
16 | "eslint-plugin-import": "^2.29.1",
17 | "eslint-plugin-node": "^11.1.0",
18 | "eslint-plugin-promise": "^4.3.1",
19 | "husky": "^8.0.3",
20 | "lint-staged": "^15.2.2",
21 | "markdownlint-cli": "^0.40.0",
22 | "rimraf": "^5.0.7"
23 | },
24 | "eslintConfig": {
25 | "extends": [
26 | "standard"
27 | ],
28 | "ignorePatterns": [
29 | "node_modules",
30 | "testApp/packages"
31 | ],
32 | "rules": {
33 | "semi": [
34 | 2,
35 | "always"
36 | ],
37 | "comma-dangle": [
38 | 2,
39 | "always-multiline"
40 | ]
41 | },
42 | "parser": "@babel/eslint-parser",
43 | "parserOptions": {
44 | "requireConfigFile": false
45 | },
46 | "env": {
47 | "browser": true,
48 | "node": true,
49 | "es6": true
50 | }
51 | },
52 | "lint-staged": {
53 | "*.js": "eslint --cache --fix",
54 | "*.md": "markdownlint -f"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/testApp/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | packages/
3 |
--------------------------------------------------------------------------------
/testApp/.meteor/.finished-upgraders:
--------------------------------------------------------------------------------
1 | # This file contains information which helps Meteor properly upgrade your
2 | # app when you run 'meteor update'. You should check it into version control
3 | # with your project.
4 |
5 | notices-for-0.9.0
6 | notices-for-0.9.1
7 | 0.9.4-platform-file
8 | notices-for-facebook-graph-api-2
9 | 1.2.0-standard-minifiers-package
10 | 1.2.0-meteor-platform-split
11 | 1.2.0-cordova-changes
12 | 1.2.0-breaking-changes
13 | 1.3.0-split-minifiers-package
14 | 1.4.0-remove-old-dev-bundle-link
15 | 1.4.1-add-shell-server-package
16 | 1.4.3-split-account-service-packages
17 | 1.5-add-dynamic-import-package
18 | 1.7-split-underscore-from-meteor-base
19 | 1.8.3-split-jquery-from-blaze
20 |
--------------------------------------------------------------------------------
/testApp/.meteor/.gitignore:
--------------------------------------------------------------------------------
1 | local
2 |
--------------------------------------------------------------------------------
/testApp/.meteor/.id:
--------------------------------------------------------------------------------
1 | # This file contains a token that is unique to your project.
2 | # Check it into your repository along with the rest of this directory.
3 | # It can be used for purposes such as:
4 | # - ensuring you don't accidentally deploy one app on top of another
5 | # - providing package authors with aggregated statistics
6 |
7 | s780pthnolgq.zjiijcbe1ln
8 |
--------------------------------------------------------------------------------
/testApp/.meteor/packages:
--------------------------------------------------------------------------------
1 | # Meteor packages used by this project, one per line.
2 | # Check this file (and the other files in this directory) into your repository.
3 | #
4 | # 'meteor add' and 'meteor remove' will edit this file for you,
5 | # but you can also edit it by hand.
6 |
7 | meteor-base@1.4.0 # Packages every Meteor app needs to have
8 | mobile-experience@1.1.0 # Packages for a great mobile UX
9 | mongo@1.11.0 # The database Meteor supports right now
10 | static-html # Define static page content in .html files
11 | reactive-var@1.0.11 # Reactive variable for tracker
12 | tracker@1.2.0 # Meteor's client-side reactive programming library
13 |
14 | standard-minifier-css@1.7.2 # CSS minifier run for production mode
15 | standard-minifier-js@2.6.0 # JS minifier run for production mode
16 | es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers
17 | ecmascript@0.15.1 # Enable ECMAScript2015+ syntax in app code
18 | typescript@4.2.2 # Enable TypeScript syntax in .ts and .tsx modules
19 | shell-server@0.5.0 # Server-side component of the `meteor shell` command
20 | communitypackages:fast-render
21 |
--------------------------------------------------------------------------------
/testApp/.meteor/platforms:
--------------------------------------------------------------------------------
1 | server
2 | browser
3 |
--------------------------------------------------------------------------------
/testApp/.meteor/release:
--------------------------------------------------------------------------------
1 | METEOR@2.2
2 |
--------------------------------------------------------------------------------
/testApp/.meteor/versions:
--------------------------------------------------------------------------------
1 | accounts-base@1.9.0
2 | allow-deny@1.1.0
3 | autoupdate@1.7.0
4 | babel-compiler@7.6.1
5 | babel-runtime@1.5.0
6 | base64@1.0.12
7 | binary-heap@1.0.11
8 | blaze-tools@1.1.1
9 | boilerplate-generator@1.7.1
10 | caching-compiler@1.2.2
11 | caching-html-compiler@1.2.0
12 | callback-hook@1.3.0
13 | check@1.3.1
14 | communitypackages:fast-render@4.0.0
15 | communitypackages:inject-data@2.3.2
16 | communitypackages:picker@1.1.0
17 | ddp@1.4.0
18 | ddp-client@2.4.1
19 | ddp-common@1.4.0
20 | ddp-rate-limiter@1.0.9
21 | ddp-server@2.3.3
22 | diff-sequence@1.1.1
23 | dynamic-import@0.6.0
24 | ecmascript@0.15.1
25 | ecmascript-runtime@0.7.0
26 | ecmascript-runtime-client@0.11.0
27 | ecmascript-runtime-server@0.10.0
28 | ejson@1.1.1
29 | es5-shim@4.8.0
30 | fetch@0.1.1
31 | geojson-utils@1.0.10
32 | hot-code-push@1.0.4
33 | html-tools@1.1.1
34 | htmljs@1.1.0
35 | http@1.0.10
36 | id-map@1.1.0
37 | inter-process-messaging@0.1.1
38 | lamhieu:meteorx@2.0.1
39 | launch-screen@1.2.1
40 | livedata@1.0.18
41 | localstorage@1.2.0
42 | logging@1.2.0
43 | meteor@1.9.3
44 | meteor-base@1.4.0
45 | meteortesting:browser-tests@1.3.4
46 | meteortesting:mocha@2.0.1
47 | meteortesting:mocha-core@8.0.1
48 | minifier-css@1.5.4
49 | minifier-js@2.6.0
50 | minimongo@1.6.2
51 | mobile-experience@1.1.0
52 | mobile-status-bar@1.1.0
53 | modern-browsers@0.1.5
54 | modules@0.16.0
55 | modules-runtime@0.12.0
56 | mongo@1.11.1
57 | mongo-decimal@0.1.2
58 | mongo-dev-server@1.1.0
59 | mongo-id@1.0.7
60 | npm-mongo@3.9.0
61 | ordered-dict@1.1.0
62 | promise@0.11.2
63 | random@1.2.0
64 | rate-limit@1.0.9
65 | react-fast-refresh@0.1.0
66 | reactive-var@1.0.11
67 | reload@1.3.1
68 | retry@1.1.0
69 | routepolicy@1.1.0
70 | server-render@0.3.1
71 | service-configuration@1.0.11
72 | shell-server@0.5.0
73 | socket-stream-client@0.3.2
74 | spacebars-compiler@1.2.1
75 | standard-minifier-css@1.7.2
76 | standard-minifier-js@2.6.0
77 | static-html@1.3.1
78 | templating-tools@1.2.0
79 | tracker@1.2.0
80 | typescript@4.2.2
81 | underscore@1.0.10
82 | url@1.3.1
83 | webapp@1.10.1
84 | webapp-hashing@1.1.0
85 |
--------------------------------------------------------------------------------
/testApp/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testapp",
3 | "requires": true,
4 | "lockfileVersion": 1,
5 | "dependencies": {
6 | "@babel/runtime": {
7 | "version": "7.14.0",
8 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz",
9 | "integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==",
10 | "requires": {
11 | "regenerator-runtime": "^0.13.4"
12 | }
13 | },
14 | "@types/node": {
15 | "version": "15.0.2",
16 | "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
17 | "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==",
18 | "optional": true
19 | },
20 | "@types/yauzl": {
21 | "version": "2.9.1",
22 | "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz",
23 | "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==",
24 | "optional": true,
25 | "requires": {
26 | "@types/node": "*"
27 | }
28 | },
29 | "agent-base": {
30 | "version": "6.0.2",
31 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
32 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
33 | "requires": {
34 | "debug": "4"
35 | }
36 | },
37 | "assertion-error": {
38 | "version": "1.1.0",
39 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
40 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="
41 | },
42 | "balanced-match": {
43 | "version": "1.0.2",
44 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
45 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
46 | },
47 | "base64-js": {
48 | "version": "1.5.1",
49 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
50 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
51 | },
52 | "bl": {
53 | "version": "4.1.0",
54 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
55 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
56 | "requires": {
57 | "buffer": "^5.5.0",
58 | "inherits": "^2.0.4",
59 | "readable-stream": "^3.4.0"
60 | }
61 | },
62 | "brace-expansion": {
63 | "version": "1.1.11",
64 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
65 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
66 | "requires": {
67 | "balanced-match": "^1.0.0",
68 | "concat-map": "0.0.1"
69 | }
70 | },
71 | "buffer": {
72 | "version": "5.7.1",
73 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
74 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
75 | "requires": {
76 | "base64-js": "^1.3.1",
77 | "ieee754": "^1.1.13"
78 | }
79 | },
80 | "buffer-crc32": {
81 | "version": "0.2.13",
82 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
83 | "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
84 | },
85 | "chai": {
86 | "version": "4.3.4",
87 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz",
88 | "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==",
89 | "requires": {
90 | "assertion-error": "^1.1.0",
91 | "check-error": "^1.0.2",
92 | "deep-eql": "^3.0.1",
93 | "get-func-name": "^2.0.0",
94 | "pathval": "^1.1.1",
95 | "type-detect": "^4.0.5"
96 | }
97 | },
98 | "check-error": {
99 | "version": "1.0.2",
100 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
101 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII="
102 | },
103 | "chownr": {
104 | "version": "1.1.4",
105 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
106 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
107 | },
108 | "concat-map": {
109 | "version": "0.0.1",
110 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
111 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
112 | },
113 | "debug": {
114 | "version": "4.3.1",
115 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
116 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
117 | "requires": {
118 | "ms": "2.1.2"
119 | }
120 | },
121 | "deep-eql": {
122 | "version": "3.0.1",
123 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
124 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
125 | "requires": {
126 | "type-detect": "^4.0.0"
127 | }
128 | },
129 | "devtools-protocol": {
130 | "version": "0.0.869402",
131 | "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz",
132 | "integrity": "sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA=="
133 | },
134 | "end-of-stream": {
135 | "version": "1.4.4",
136 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
137 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
138 | "requires": {
139 | "once": "^1.4.0"
140 | }
141 | },
142 | "extract-zip": {
143 | "version": "2.0.1",
144 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
145 | "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
146 | "requires": {
147 | "@types/yauzl": "^2.9.1",
148 | "debug": "^4.1.1",
149 | "get-stream": "^5.1.0",
150 | "yauzl": "^2.10.0"
151 | }
152 | },
153 | "fd-slicer": {
154 | "version": "1.1.0",
155 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
156 | "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
157 | "requires": {
158 | "pend": "~1.2.0"
159 | }
160 | },
161 | "find-up": {
162 | "version": "4.1.0",
163 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
164 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
165 | "requires": {
166 | "locate-path": "^5.0.0",
167 | "path-exists": "^4.0.0"
168 | }
169 | },
170 | "fs-constants": {
171 | "version": "1.0.0",
172 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
173 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
174 | },
175 | "fs.realpath": {
176 | "version": "1.0.0",
177 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
178 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
179 | },
180 | "get-func-name": {
181 | "version": "2.0.0",
182 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
183 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE="
184 | },
185 | "get-stream": {
186 | "version": "5.2.0",
187 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
188 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
189 | "requires": {
190 | "pump": "^3.0.0"
191 | }
192 | },
193 | "glob": {
194 | "version": "7.1.7",
195 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
196 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
197 | "requires": {
198 | "fs.realpath": "^1.0.0",
199 | "inflight": "^1.0.4",
200 | "inherits": "2",
201 | "minimatch": "^3.0.4",
202 | "once": "^1.3.0",
203 | "path-is-absolute": "^1.0.0"
204 | }
205 | },
206 | "https-proxy-agent": {
207 | "version": "5.0.0",
208 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
209 | "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
210 | "requires": {
211 | "agent-base": "6",
212 | "debug": "4"
213 | }
214 | },
215 | "ieee754": {
216 | "version": "1.2.1",
217 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
218 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
219 | },
220 | "inflight": {
221 | "version": "1.0.6",
222 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
223 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
224 | "requires": {
225 | "once": "^1.3.0",
226 | "wrappy": "1"
227 | }
228 | },
229 | "inherits": {
230 | "version": "2.0.4",
231 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
232 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
233 | },
234 | "locate-path": {
235 | "version": "5.0.0",
236 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
237 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
238 | "requires": {
239 | "p-locate": "^4.1.0"
240 | }
241 | },
242 | "meteor-node-stubs": {
243 | "version": "1.0.3",
244 | "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-1.0.3.tgz",
245 | "integrity": "sha512-JQwIWPfM/Oa2x1Ycwn1Q0wVVQ8b0bOLv+qs4RR/D12b5dPktLlPCRhMzWzRPncZVJtfsnKKBgPLdFmJYUqAwHg==",
246 | "requires": {
247 | "assert": "^1.4.1",
248 | "browserify-zlib": "^0.2.0",
249 | "buffer": "^5.2.1",
250 | "console-browserify": "^1.1.0",
251 | "constants-browserify": "^1.0.0",
252 | "crypto-browserify": "^3.12.0",
253 | "domain-browser": "^1.2.0",
254 | "elliptic": "^6.5.4",
255 | "events": "^3.0.0",
256 | "https-browserify": "^1.0.0",
257 | "os-browserify": "^0.3.0",
258 | "path-browserify": "^1.0.0",
259 | "process": "^0.11.10",
260 | "punycode": "^2.1.1",
261 | "querystring-es3": "^0.2.1",
262 | "readable-stream": "^3.3.0",
263 | "stream-browserify": "^2.0.2",
264 | "stream-http": "^3.0.0",
265 | "string_decoder": "^1.2.0",
266 | "timers-browserify": "^2.0.10",
267 | "tty-browserify": "0.0.1",
268 | "url": "^0.11.0",
269 | "util": "^0.11.1",
270 | "vm-browserify": "^1.1.0"
271 | },
272 | "dependencies": {
273 | "asn1.js": {
274 | "version": "5.4.1",
275 | "bundled": true,
276 | "requires": {
277 | "bn.js": "^4.0.0",
278 | "inherits": "^2.0.1",
279 | "minimalistic-assert": "^1.0.0",
280 | "safer-buffer": "^2.1.0"
281 | },
282 | "dependencies": {
283 | "bn.js": {
284 | "version": "4.12.0",
285 | "bundled": true
286 | }
287 | }
288 | },
289 | "assert": {
290 | "version": "1.5.0",
291 | "bundled": true,
292 | "requires": {
293 | "object-assign": "^4.1.1",
294 | "util": "0.10.3"
295 | },
296 | "dependencies": {
297 | "util": {
298 | "version": "0.10.3",
299 | "bundled": true,
300 | "requires": {
301 | "inherits": "2.0.1"
302 | }
303 | }
304 | }
305 | },
306 | "base64-js": {
307 | "version": "1.5.1",
308 | "bundled": true
309 | },
310 | "bn.js": {
311 | "version": "5.2.0",
312 | "bundled": true
313 | },
314 | "brorand": {
315 | "version": "1.1.0",
316 | "bundled": true
317 | },
318 | "browserify-aes": {
319 | "version": "1.2.0",
320 | "bundled": true,
321 | "requires": {
322 | "buffer-xor": "^1.0.3",
323 | "cipher-base": "^1.0.0",
324 | "create-hash": "^1.1.0",
325 | "evp_bytestokey": "^1.0.3",
326 | "inherits": "^2.0.1",
327 | "safe-buffer": "^5.0.1"
328 | }
329 | },
330 | "browserify-cipher": {
331 | "version": "1.0.1",
332 | "bundled": true,
333 | "requires": {
334 | "browserify-aes": "^1.0.4",
335 | "browserify-des": "^1.0.0",
336 | "evp_bytestokey": "^1.0.0"
337 | }
338 | },
339 | "browserify-des": {
340 | "version": "1.0.2",
341 | "bundled": true,
342 | "requires": {
343 | "cipher-base": "^1.0.1",
344 | "des.js": "^1.0.0",
345 | "inherits": "^2.0.1",
346 | "safe-buffer": "^5.1.2"
347 | }
348 | },
349 | "browserify-rsa": {
350 | "version": "4.1.0",
351 | "bundled": true,
352 | "requires": {
353 | "bn.js": "^5.0.0",
354 | "randombytes": "^2.0.1"
355 | }
356 | },
357 | "browserify-sign": {
358 | "version": "4.2.1",
359 | "bundled": true,
360 | "requires": {
361 | "bn.js": "^5.1.1",
362 | "browserify-rsa": "^4.0.1",
363 | "create-hash": "^1.2.0",
364 | "create-hmac": "^1.1.7",
365 | "elliptic": "^6.5.3",
366 | "inherits": "^2.0.4",
367 | "parse-asn1": "^5.1.5",
368 | "readable-stream": "^3.6.0",
369 | "safe-buffer": "^5.2.0"
370 | },
371 | "dependencies": {
372 | "inherits": {
373 | "version": "2.0.4",
374 | "bundled": true
375 | }
376 | }
377 | },
378 | "browserify-zlib": {
379 | "version": "0.2.0",
380 | "bundled": true,
381 | "requires": {
382 | "pako": "~1.0.5"
383 | }
384 | },
385 | "buffer": {
386 | "version": "5.7.1",
387 | "bundled": true,
388 | "requires": {
389 | "base64-js": "^1.3.1",
390 | "ieee754": "^1.1.13"
391 | }
392 | },
393 | "buffer-xor": {
394 | "version": "1.0.3",
395 | "bundled": true
396 | },
397 | "builtin-status-codes": {
398 | "version": "3.0.0",
399 | "bundled": true
400 | },
401 | "cipher-base": {
402 | "version": "1.0.4",
403 | "bundled": true,
404 | "requires": {
405 | "inherits": "^2.0.1",
406 | "safe-buffer": "^5.0.1"
407 | }
408 | },
409 | "console-browserify": {
410 | "version": "1.2.0",
411 | "bundled": true
412 | },
413 | "constants-browserify": {
414 | "version": "1.0.0",
415 | "bundled": true
416 | },
417 | "core-util-is": {
418 | "version": "1.0.2",
419 | "bundled": true
420 | },
421 | "create-ecdh": {
422 | "version": "4.0.4",
423 | "bundled": true,
424 | "requires": {
425 | "bn.js": "^4.1.0",
426 | "elliptic": "^6.5.3"
427 | },
428 | "dependencies": {
429 | "bn.js": {
430 | "version": "4.12.0",
431 | "bundled": true
432 | }
433 | }
434 | },
435 | "create-hash": {
436 | "version": "1.2.0",
437 | "bundled": true,
438 | "requires": {
439 | "cipher-base": "^1.0.1",
440 | "inherits": "^2.0.1",
441 | "md5.js": "^1.3.4",
442 | "ripemd160": "^2.0.1",
443 | "sha.js": "^2.4.0"
444 | }
445 | },
446 | "create-hmac": {
447 | "version": "1.1.7",
448 | "bundled": true,
449 | "requires": {
450 | "cipher-base": "^1.0.3",
451 | "create-hash": "^1.1.0",
452 | "inherits": "^2.0.1",
453 | "ripemd160": "^2.0.0",
454 | "safe-buffer": "^5.0.1",
455 | "sha.js": "^2.4.8"
456 | }
457 | },
458 | "crypto-browserify": {
459 | "version": "3.12.0",
460 | "bundled": true,
461 | "requires": {
462 | "browserify-cipher": "^1.0.0",
463 | "browserify-sign": "^4.0.0",
464 | "create-ecdh": "^4.0.0",
465 | "create-hash": "^1.1.0",
466 | "create-hmac": "^1.1.0",
467 | "diffie-hellman": "^5.0.0",
468 | "inherits": "^2.0.1",
469 | "pbkdf2": "^3.0.3",
470 | "public-encrypt": "^4.0.0",
471 | "randombytes": "^2.0.0",
472 | "randomfill": "^1.0.3"
473 | }
474 | },
475 | "des.js": {
476 | "version": "1.0.1",
477 | "bundled": true,
478 | "requires": {
479 | "inherits": "^2.0.1",
480 | "minimalistic-assert": "^1.0.0"
481 | }
482 | },
483 | "diffie-hellman": {
484 | "version": "5.0.3",
485 | "bundled": true,
486 | "requires": {
487 | "bn.js": "^4.1.0",
488 | "miller-rabin": "^4.0.0",
489 | "randombytes": "^2.0.0"
490 | },
491 | "dependencies": {
492 | "bn.js": {
493 | "version": "4.12.0",
494 | "bundled": true
495 | }
496 | }
497 | },
498 | "domain-browser": {
499 | "version": "1.2.0",
500 | "bundled": true
501 | },
502 | "elliptic": {
503 | "version": "6.5.4",
504 | "bundled": true,
505 | "requires": {
506 | "bn.js": "^4.11.9",
507 | "brorand": "^1.1.0",
508 | "hash.js": "^1.0.0",
509 | "hmac-drbg": "^1.0.1",
510 | "inherits": "^2.0.4",
511 | "minimalistic-assert": "^1.0.1",
512 | "minimalistic-crypto-utils": "^1.0.1"
513 | },
514 | "dependencies": {
515 | "bn.js": {
516 | "version": "4.12.0",
517 | "bundled": true
518 | },
519 | "inherits": {
520 | "version": "2.0.4",
521 | "bundled": true
522 | }
523 | }
524 | },
525 | "events": {
526 | "version": "3.3.0",
527 | "bundled": true
528 | },
529 | "evp_bytestokey": {
530 | "version": "1.0.3",
531 | "bundled": true,
532 | "requires": {
533 | "md5.js": "^1.3.4",
534 | "safe-buffer": "^5.1.1"
535 | }
536 | },
537 | "hash-base": {
538 | "version": "3.1.0",
539 | "bundled": true,
540 | "requires": {
541 | "inherits": "^2.0.4",
542 | "readable-stream": "^3.6.0",
543 | "safe-buffer": "^5.2.0"
544 | },
545 | "dependencies": {
546 | "inherits": {
547 | "version": "2.0.4",
548 | "bundled": true
549 | }
550 | }
551 | },
552 | "hash.js": {
553 | "version": "1.1.7",
554 | "bundled": true,
555 | "requires": {
556 | "inherits": "^2.0.3",
557 | "minimalistic-assert": "^1.0.1"
558 | },
559 | "dependencies": {
560 | "inherits": {
561 | "version": "2.0.4",
562 | "bundled": true
563 | }
564 | }
565 | },
566 | "hmac-drbg": {
567 | "version": "1.0.1",
568 | "bundled": true,
569 | "requires": {
570 | "hash.js": "^1.0.3",
571 | "minimalistic-assert": "^1.0.0",
572 | "minimalistic-crypto-utils": "^1.0.1"
573 | }
574 | },
575 | "https-browserify": {
576 | "version": "1.0.0",
577 | "bundled": true
578 | },
579 | "ieee754": {
580 | "version": "1.2.1",
581 | "bundled": true
582 | },
583 | "inherits": {
584 | "version": "2.0.1",
585 | "bundled": true
586 | },
587 | "isarray": {
588 | "version": "1.0.0",
589 | "bundled": true
590 | },
591 | "md5.js": {
592 | "version": "1.3.5",
593 | "bundled": true,
594 | "requires": {
595 | "hash-base": "^3.0.0",
596 | "inherits": "^2.0.1",
597 | "safe-buffer": "^5.1.2"
598 | }
599 | },
600 | "miller-rabin": {
601 | "version": "4.0.1",
602 | "bundled": true,
603 | "requires": {
604 | "bn.js": "^4.0.0",
605 | "brorand": "^1.0.1"
606 | },
607 | "dependencies": {
608 | "bn.js": {
609 | "version": "4.12.0",
610 | "bundled": true
611 | }
612 | }
613 | },
614 | "minimalistic-assert": {
615 | "version": "1.0.1",
616 | "bundled": true
617 | },
618 | "minimalistic-crypto-utils": {
619 | "version": "1.0.1",
620 | "bundled": true
621 | },
622 | "object-assign": {
623 | "version": "4.1.1",
624 | "bundled": true
625 | },
626 | "os-browserify": {
627 | "version": "0.3.0",
628 | "bundled": true
629 | },
630 | "pako": {
631 | "version": "1.0.11",
632 | "bundled": true
633 | },
634 | "parse-asn1": {
635 | "version": "5.1.6",
636 | "bundled": true,
637 | "requires": {
638 | "asn1.js": "^5.2.0",
639 | "browserify-aes": "^1.0.0",
640 | "evp_bytestokey": "^1.0.0",
641 | "pbkdf2": "^3.0.3",
642 | "safe-buffer": "^5.1.1"
643 | }
644 | },
645 | "path-browserify": {
646 | "version": "1.0.1",
647 | "bundled": true
648 | },
649 | "pbkdf2": {
650 | "version": "3.1.1",
651 | "bundled": true,
652 | "requires": {
653 | "create-hash": "^1.1.2",
654 | "create-hmac": "^1.1.4",
655 | "ripemd160": "^2.0.1",
656 | "safe-buffer": "^5.0.1",
657 | "sha.js": "^2.4.8"
658 | }
659 | },
660 | "process": {
661 | "version": "0.11.10",
662 | "bundled": true
663 | },
664 | "process-nextick-args": {
665 | "version": "2.0.1",
666 | "bundled": true
667 | },
668 | "public-encrypt": {
669 | "version": "4.0.3",
670 | "bundled": true,
671 | "requires": {
672 | "bn.js": "^4.1.0",
673 | "browserify-rsa": "^4.0.0",
674 | "create-hash": "^1.1.0",
675 | "parse-asn1": "^5.0.0",
676 | "randombytes": "^2.0.1",
677 | "safe-buffer": "^5.1.2"
678 | },
679 | "dependencies": {
680 | "bn.js": {
681 | "version": "4.12.0",
682 | "bundled": true
683 | }
684 | }
685 | },
686 | "punycode": {
687 | "version": "2.1.1",
688 | "bundled": true
689 | },
690 | "querystring": {
691 | "version": "0.2.0",
692 | "bundled": true
693 | },
694 | "querystring-es3": {
695 | "version": "0.2.1",
696 | "bundled": true
697 | },
698 | "randombytes": {
699 | "version": "2.1.0",
700 | "bundled": true,
701 | "requires": {
702 | "safe-buffer": "^5.1.0"
703 | }
704 | },
705 | "randomfill": {
706 | "version": "1.0.4",
707 | "bundled": true,
708 | "requires": {
709 | "randombytes": "^2.0.5",
710 | "safe-buffer": "^5.1.0"
711 | }
712 | },
713 | "readable-stream": {
714 | "version": "3.6.0",
715 | "bundled": true,
716 | "requires": {
717 | "inherits": "^2.0.3",
718 | "string_decoder": "^1.1.1",
719 | "util-deprecate": "^1.0.1"
720 | },
721 | "dependencies": {
722 | "inherits": {
723 | "version": "2.0.4",
724 | "bundled": true
725 | }
726 | }
727 | },
728 | "ripemd160": {
729 | "version": "2.0.2",
730 | "bundled": true,
731 | "requires": {
732 | "hash-base": "^3.0.0",
733 | "inherits": "^2.0.1"
734 | }
735 | },
736 | "safe-buffer": {
737 | "version": "5.2.1",
738 | "bundled": true
739 | },
740 | "safer-buffer": {
741 | "version": "2.1.2",
742 | "bundled": true
743 | },
744 | "setimmediate": {
745 | "version": "1.0.5",
746 | "bundled": true
747 | },
748 | "sha.js": {
749 | "version": "2.4.11",
750 | "bundled": true,
751 | "requires": {
752 | "inherits": "^2.0.1",
753 | "safe-buffer": "^5.0.1"
754 | }
755 | },
756 | "stream-browserify": {
757 | "version": "2.0.2",
758 | "bundled": true,
759 | "requires": {
760 | "inherits": "~2.0.1",
761 | "readable-stream": "^2.0.2"
762 | },
763 | "dependencies": {
764 | "readable-stream": {
765 | "version": "2.3.7",
766 | "bundled": true,
767 | "requires": {
768 | "core-util-is": "~1.0.0",
769 | "inherits": "~2.0.3",
770 | "isarray": "~1.0.0",
771 | "process-nextick-args": "~2.0.0",
772 | "safe-buffer": "~5.1.1",
773 | "string_decoder": "~1.1.1",
774 | "util-deprecate": "~1.0.1"
775 | },
776 | "dependencies": {
777 | "inherits": {
778 | "version": "2.0.4",
779 | "bundled": true
780 | }
781 | }
782 | },
783 | "safe-buffer": {
784 | "version": "5.1.2",
785 | "bundled": true
786 | },
787 | "string_decoder": {
788 | "version": "1.1.1",
789 | "bundled": true,
790 | "requires": {
791 | "safe-buffer": "~5.1.0"
792 | }
793 | }
794 | }
795 | },
796 | "stream-http": {
797 | "version": "3.1.1",
798 | "bundled": true,
799 | "requires": {
800 | "builtin-status-codes": "^3.0.0",
801 | "inherits": "^2.0.4",
802 | "readable-stream": "^3.6.0",
803 | "xtend": "^4.0.2"
804 | },
805 | "dependencies": {
806 | "inherits": {
807 | "version": "2.0.4",
808 | "bundled": true
809 | }
810 | }
811 | },
812 | "string_decoder": {
813 | "version": "1.3.0",
814 | "bundled": true,
815 | "requires": {
816 | "safe-buffer": "~5.2.0"
817 | }
818 | },
819 | "timers-browserify": {
820 | "version": "2.0.12",
821 | "bundled": true,
822 | "requires": {
823 | "setimmediate": "^1.0.4"
824 | }
825 | },
826 | "tty-browserify": {
827 | "version": "0.0.1",
828 | "bundled": true
829 | },
830 | "url": {
831 | "version": "0.11.0",
832 | "bundled": true,
833 | "requires": {
834 | "punycode": "1.3.2",
835 | "querystring": "0.2.0"
836 | },
837 | "dependencies": {
838 | "punycode": {
839 | "version": "1.3.2",
840 | "bundled": true
841 | }
842 | }
843 | },
844 | "util": {
845 | "version": "0.11.1",
846 | "bundled": true,
847 | "requires": {
848 | "inherits": "2.0.3"
849 | },
850 | "dependencies": {
851 | "inherits": {
852 | "version": "2.0.3",
853 | "bundled": true
854 | }
855 | }
856 | },
857 | "util-deprecate": {
858 | "version": "1.0.2",
859 | "bundled": true
860 | },
861 | "vm-browserify": {
862 | "version": "1.1.2",
863 | "bundled": true
864 | },
865 | "xtend": {
866 | "version": "4.0.2",
867 | "bundled": true
868 | }
869 | }
870 | },
871 | "minimatch": {
872 | "version": "3.0.4",
873 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
874 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
875 | "requires": {
876 | "brace-expansion": "^1.1.7"
877 | }
878 | },
879 | "mkdirp-classic": {
880 | "version": "0.5.3",
881 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
882 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
883 | },
884 | "ms": {
885 | "version": "2.1.2",
886 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
887 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
888 | },
889 | "node-fetch": {
890 | "version": "2.6.1",
891 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
892 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
893 | },
894 | "once": {
895 | "version": "1.4.0",
896 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
897 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
898 | "requires": {
899 | "wrappy": "1"
900 | }
901 | },
902 | "p-limit": {
903 | "version": "2.3.0",
904 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
905 | "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
906 | "requires": {
907 | "p-try": "^2.0.0"
908 | }
909 | },
910 | "p-locate": {
911 | "version": "4.1.0",
912 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
913 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
914 | "requires": {
915 | "p-limit": "^2.2.0"
916 | }
917 | },
918 | "p-try": {
919 | "version": "2.2.0",
920 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
921 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
922 | },
923 | "path-exists": {
924 | "version": "4.0.0",
925 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
926 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
927 | },
928 | "path-is-absolute": {
929 | "version": "1.0.1",
930 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
931 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
932 | },
933 | "pathval": {
934 | "version": "1.1.1",
935 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
936 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="
937 | },
938 | "pend": {
939 | "version": "1.2.0",
940 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
941 | "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
942 | },
943 | "pkg-dir": {
944 | "version": "4.2.0",
945 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
946 | "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
947 | "requires": {
948 | "find-up": "^4.0.0"
949 | }
950 | },
951 | "progress": {
952 | "version": "2.0.3",
953 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
954 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
955 | },
956 | "proxy-from-env": {
957 | "version": "1.1.0",
958 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
959 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
960 | },
961 | "pump": {
962 | "version": "3.0.0",
963 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
964 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
965 | "requires": {
966 | "end-of-stream": "^1.1.0",
967 | "once": "^1.3.1"
968 | }
969 | },
970 | "puppeteer": {
971 | "version": "9.1.1",
972 | "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-9.1.1.tgz",
973 | "integrity": "sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw==",
974 | "requires": {
975 | "debug": "^4.1.0",
976 | "devtools-protocol": "0.0.869402",
977 | "extract-zip": "^2.0.0",
978 | "https-proxy-agent": "^5.0.0",
979 | "node-fetch": "^2.6.1",
980 | "pkg-dir": "^4.2.0",
981 | "progress": "^2.0.1",
982 | "proxy-from-env": "^1.1.0",
983 | "rimraf": "^3.0.2",
984 | "tar-fs": "^2.0.0",
985 | "unbzip2-stream": "^1.3.3",
986 | "ws": "^7.2.3"
987 | }
988 | },
989 | "readable-stream": {
990 | "version": "3.6.0",
991 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
992 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
993 | "requires": {
994 | "inherits": "^2.0.3",
995 | "string_decoder": "^1.1.1",
996 | "util-deprecate": "^1.0.1"
997 | }
998 | },
999 | "regenerator-runtime": {
1000 | "version": "0.13.7",
1001 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
1002 | "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
1003 | },
1004 | "rimraf": {
1005 | "version": "3.0.2",
1006 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
1007 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
1008 | "requires": {
1009 | "glob": "^7.1.3"
1010 | }
1011 | },
1012 | "safe-buffer": {
1013 | "version": "5.2.1",
1014 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1015 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
1016 | },
1017 | "string_decoder": {
1018 | "version": "1.3.0",
1019 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
1020 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
1021 | "requires": {
1022 | "safe-buffer": "~5.2.0"
1023 | }
1024 | },
1025 | "tar-fs": {
1026 | "version": "2.1.1",
1027 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
1028 | "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
1029 | "requires": {
1030 | "chownr": "^1.1.1",
1031 | "mkdirp-classic": "^0.5.2",
1032 | "pump": "^3.0.0",
1033 | "tar-stream": "^2.1.4"
1034 | }
1035 | },
1036 | "tar-stream": {
1037 | "version": "2.2.0",
1038 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
1039 | "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
1040 | "requires": {
1041 | "bl": "^4.0.3",
1042 | "end-of-stream": "^1.4.1",
1043 | "fs-constants": "^1.0.0",
1044 | "inherits": "^2.0.3",
1045 | "readable-stream": "^3.1.1"
1046 | }
1047 | },
1048 | "through": {
1049 | "version": "2.3.8",
1050 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
1051 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
1052 | },
1053 | "type-detect": {
1054 | "version": "4.0.8",
1055 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
1056 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="
1057 | },
1058 | "unbzip2-stream": {
1059 | "version": "1.4.3",
1060 | "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
1061 | "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
1062 | "requires": {
1063 | "buffer": "^5.2.1",
1064 | "through": "^2.3.8"
1065 | }
1066 | },
1067 | "util-deprecate": {
1068 | "version": "1.0.2",
1069 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
1070 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
1071 | },
1072 | "wrappy": {
1073 | "version": "1.0.2",
1074 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1075 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
1076 | },
1077 | "ws": {
1078 | "version": "7.4.5",
1079 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
1080 | "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g=="
1081 | },
1082 | "yauzl": {
1083 | "version": "2.10.0",
1084 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
1085 | "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=",
1086 | "requires": {
1087 | "buffer-crc32": "~0.2.3",
1088 | "fd-slicer": "~1.1.0"
1089 | }
1090 | }
1091 | }
1092 | }
1093 |
--------------------------------------------------------------------------------
/testApp/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testapp",
3 | "private": true,
4 | "scripts": {
5 | "start": "meteor run",
6 | "setup": "meteor npm install && mkdir -p packages && ln -sfn ../../ ./packages/meteor-fast-render",
7 | "test": "METEOR_PACKAGE_DIRS='../' TEST_BROWSER_DRIVER=puppeteer meteor test-packages --once --raw-logs --driver-package meteortesting:mocha ../",
8 | "test:watch": "METEOR_PACKAGE_DIRS='../' TEST_BROWSER_DRIVER=puppeteer TEST_WATCH=1 meteor test-packages --raw-logs --driver-package meteortesting:mocha ../"
9 | },
10 | "dependencies": {
11 | "@babel/runtime": "^7.11.2",
12 | "chai": "^4.3.4",
13 | "meteor-node-stubs": "^1.0.1",
14 | "puppeteer": "^9.1.1"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/client/ddp_update.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | import { FastRender } from 'meteor/communitypackages:fast-render';
3 | import { Random } from 'meteor/random';
4 | import { Meteor } from 'meteor/meteor';
5 | import { Mongo } from 'meteor/mongo';
6 | import { EJSON } from 'meteor/ejson';
7 | import { assert } from 'chai';
8 |
9 | const bufferedWritesInterval = 5;
10 |
11 | describe('DDPUpdate', function () {
12 | it('convert added to changed', function (done) {
13 | const collName = Random.id();
14 | const coll = new Mongo.Collection(collName);
15 |
16 | Meteor.connection._livedata_data({
17 | msg: 'added',
18 | collection: collName,
19 | id: 'one',
20 | fields: { name: 'arunoda' },
21 | });
22 |
23 | Meteor.setTimeout(function () {
24 | assert.deepEqual(coll.findOne('one'), { _id: 'one', name: 'arunoda' });
25 |
26 | Meteor.connection._livedata_data({
27 | msg: 'added',
28 | collection: collName,
29 | id: 'one',
30 | fields: { name: 'kuma', age: 20 },
31 | });
32 |
33 | Meteor.setTimeout(function () {
34 | assert.deepEqual(coll.findOne('one'), { _id: 'one', name: 'kuma', age: 20 });
35 | done();
36 | }, bufferedWritesInterval);
37 | }, bufferedWritesInterval);
38 | });
39 |
40 | it('create collection later on', function (done) {
41 | const collName = Random.id();
42 |
43 | Meteor.connection._livedata_data({
44 | msg: 'added',
45 | collection: collName,
46 | id: 'one',
47 | fields: { name: 'arunoda' },
48 | });
49 |
50 | Meteor.connection._livedata_data({
51 | msg: 'added',
52 | collection: collName,
53 | id: 'two',
54 | fields: { name: 'kamal' },
55 | });
56 |
57 | const coll = new Mongo.Collection(collName);
58 | Meteor.setTimeout(function () {
59 | assert.equal(coll.find().fetch().length, 2);
60 | done();
61 | }, bufferedWritesInterval);
62 | });
63 |
64 | it('delete subscriptions', function () {
65 | FastRender._revertedBackToOriginal = false;
66 | const sub1 = { name: 'coola', paramsKey: 'k1' };
67 | const sub2 = { name: 'booma', paramsKey: 'k2' };
68 | FastRender._subscriptionIdMap = { subId: sub1, subId2: sub2 };
69 | FastRender._subscriptions = { coola: { k1: true }, booma: { k2: true } };
70 |
71 | Meteor.connection._livedata_data({
72 | msg: 'ready',
73 | subs: ['subId'],
74 | });
75 |
76 | FastRender._revertedBackToOriginal = true;
77 |
78 | assert.deepEqual(FastRender._subscriptionIdMap, { subId2: sub2 });
79 | assert.deepEqual(FastRender._subscriptions, { booma: { k2: true } });
80 | });
81 |
82 | it('ignore frGen ready messages', function () {
83 | FastRender._revertedBackToOriginal = false;
84 | FastRender._subscriptionIdMap = { subId: 'coola', subId2: 'coola' };
85 | FastRender._subscriptions = { coola: true, booma: true };
86 |
87 | Meteor.connection._livedata_data({
88 | msg: 'ready',
89 | subs: ['subId'],
90 | frGen: true,
91 | });
92 |
93 | FastRender._revertedBackToOriginal = true;
94 |
95 | assert.deepEqual(FastRender._subscriptionIdMap, { subId: 'coola', subId2: 'coola' });
96 | assert.deepEqual(FastRender._subscriptions, { coola: true, booma: true });
97 | });
98 |
99 | it('revertedBackToOriginal', function () {
100 | FastRender._revertedBackToOriginal = false;
101 | FastRender._subscriptionIdMap = { subId: { name: 'coola', paramsKey: 'pk' } };
102 | FastRender._subscriptions = { coola: { pk: true } };
103 |
104 | Meteor.connection._livedata_data({
105 | msg: 'ready',
106 | subs: ['subId'],
107 | });
108 |
109 | assert.deepEqual(FastRender._subscriptionIdMap, {});
110 | assert.deepEqual(FastRender._subscriptions, {});
111 | assert.equal(FastRender._revertedBackToOriginal, true);
112 | });
113 |
114 | it('fake ready messages', function () {
115 | FastRender._revertedBackToOriginal = false;
116 | const orginalSend = Meteor.connection._send;
117 |
118 | const params = [10, 20];
119 | const paramsKey = EJSON.stringify(params);
120 | FastRender._subscriptions = { coolio: {} };
121 | FastRender._subscriptions.coolio[paramsKey] = true;
122 |
123 | const subId = 'the-id';
124 | Meteor.connection._send({
125 | msg: 'sub',
126 | name: 'coolio',
127 | id: subId,
128 | params: params,
129 | });
130 | assert.deepEqual(FastRender._subscriptionIdMap, {
131 | 'the-id': { name: 'coolio', paramsKey: paramsKey },
132 | });
133 |
134 | Meteor.connection._send = orginalSend;
135 | FastRender._revertedBackToOriginal = false;
136 | });
137 | });
138 |
--------------------------------------------------------------------------------
/tests/client/fast_render.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | import { FastRender } from 'meteor/communitypackages:fast-render';
3 | import { Random } from 'meteor/random';
4 | import { Meteor } from 'meteor/meteor';
5 | import { Mongo } from 'meteor/mongo';
6 | import { assert } from 'chai';
7 |
8 | const bufferedWritesInterval = 5;
9 |
10 | describe('FastRender', function () {
11 | describe('init()', function () {
12 | it('coll data', function () {
13 | const expectedMessages = [
14 | {
15 | msg: 'added',
16 | collection: 'posts',
17 | id: 'one',
18 | fields: { name: 'arunoda' },
19 | frGen: true,
20 | },
21 | {
22 | msg: 'added',
23 | collection: 'posts',
24 | id: 'two',
25 | fields: { name: 'meteorhacks' },
26 | frGen: true,
27 | },
28 | {
29 | msg: 'added',
30 | collection: 'comments',
31 | id: 'one',
32 | fields: { text: 'great' },
33 | frGen: true,
34 | },
35 | ];
36 |
37 | const payload = {
38 | subscriptions: { posts: true },
39 | collectionData: {
40 | posts: [
41 | [{ _id: 'one', name: 'arunoda' }, { _id: 'two', name: 'meteorhacks' }],
42 | ],
43 | comments: [[{ _id: 'one', text: 'great' }]],
44 | },
45 | };
46 |
47 | const newMessages = [];
48 |
49 | WithNewInjectDdpMessage(
50 | function (conn, ddpMessage) {
51 | newMessages.push(ddpMessage);
52 | },
53 | function () {
54 | FastRender.init(payload);
55 |
56 | assert.deepEqual(newMessages, expectedMessages);
57 | assert.deepEqual(FastRender._subscriptions, payload.subscriptions);
58 | },
59 | );
60 | });
61 |
62 | it('ObjectId support', function (done) {
63 | const id = new FastRender.IDTools.ObjectID();
64 | const payload = {
65 | subscriptions: { posts: true },
66 | collectionData: {
67 | posts: [[{ _id: id, name: 'arunoda' }]],
68 | },
69 | };
70 |
71 | WithNewInjectDdpMessage(
72 | function (conn, ddpMessage) {
73 | assert.equal(ddpMessage.id, id._str);
74 | done();
75 | },
76 | function () {
77 | FastRender.init(payload);
78 | },
79 | );
80 | });
81 |
82 | it('merge docs deep', function (done) {
83 | const collName = Random.id();
84 | const payload = {
85 | subscriptions: { posts: true },
86 | collectionData: {},
87 | };
88 |
89 | payload.collectionData[collName] = [
90 | [{ _id: 'one', name: 'arunoda', profile: { name: 'arunoda' } }],
91 | [
92 | {
93 | _id: 'one',
94 | name: 'arunoda',
95 | profile: { email: 'arunoda@arunoda.com' },
96 | },
97 | ],
98 | ];
99 |
100 | FastRender.init(payload);
101 |
102 | const coll = new Mongo.Collection(collName);
103 | Meteor.setTimeout(function () {
104 | assert.deepEqual(coll.findOne('one'), {
105 | _id: 'one',
106 | name: 'arunoda',
107 | profile: {
108 | name: 'arunoda',
109 | email: 'arunoda@arunoda.com',
110 | },
111 | });
112 | done();
113 | }, bufferedWritesInterval);
114 | });
115 |
116 | it('ejson data', function (done) {
117 | const collName = Random.id();
118 | const payload = {
119 | subscriptions: { posts: true },
120 | collectionData: {},
121 | };
122 |
123 | const date = new Date('2014-10-20');
124 | payload.collectionData[collName] = [
125 | [{ _id: 'one', name: 'arunoda', date: date }],
126 | ];
127 |
128 | FastRender.init(payload);
129 |
130 | const coll = new Mongo.Collection(collName);
131 | Meteor.setTimeout(function () {
132 | const doc = coll.findOne('one');
133 | assert.equal(doc.date.getTime(), date.getTime());
134 | done();
135 | }, bufferedWritesInterval);
136 | });
137 | });
138 | });
139 |
140 | const WithNewInjectDdpMessage = function (newCallback, runCode) {
141 | const originalInjectDDP = FastRender.injectDdpMessage;
142 | FastRender.injectDdpMessage = newCallback;
143 | if (runCode) runCode();
144 | FastRender.injectDdpMessage = originalInjectDDP;
145 | };
146 |
--------------------------------------------------------------------------------
/tests/client/index.js:
--------------------------------------------------------------------------------
1 | import './ddp_update';
2 | import './fast_render';
3 | import './utils.js';
4 |
--------------------------------------------------------------------------------
/tests/client/utils.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | import { FastRender } from 'meteor/communitypackages:fast-render';
3 | import { assert } from 'chai';
4 |
5 | describe('AddedToChanged', function () {
6 | it('new fields', function () {
7 | const localCopy = { aa: 10 };
8 | const added = { fields: { aa: 20, bb: 20 }, msg: 'added' };
9 |
10 | FastRender._AddedToChanged(localCopy, added);
11 |
12 | assert.equal(added.msg, 'changed');
13 | assert.deepEqual(added.fields, { aa: 20, bb: 20 });
14 | });
15 | it('removed fields', function () {
16 | const localCopy = { aa: 10, cc: 20, bb: 10 };
17 | const added = { fields: { bb: 20 }, msg: 'added' };
18 |
19 | FastRender._AddedToChanged(localCopy, added);
20 |
21 | assert.equal(added.msg, 'changed');
22 | assert.deepEqual(added.fields, { bb: 20 });
23 | assert.deepEqual(added.cleared, ['aa', 'cc']);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/tests/server/context.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | import { FastRender } from 'meteor/communitypackages:fast-render';
3 | import { Random } from 'meteor/random';
4 | import { Meteor } from 'meteor/meteor';
5 | import { Mongo } from 'meteor/mongo';
6 | import { Accounts } from 'meteor/accounts-base';
7 | import { assert } from 'chai';
8 |
9 | describe('context', function () {
10 | it('subscribe', function () {
11 | const collName = Random.id();
12 | const coll = new Mongo.Collection(collName);
13 | coll.insert({ _id: 'one', age: 20 });
14 | coll.insert({ _id: 'two', age: 40 });
15 |
16 | const pubName = Random.id();
17 | Meteor.publish(pubName, function () {
18 | return coll.find();
19 | });
20 |
21 | const context = new FastRender._Context();
22 | context.subscribe(pubName);
23 |
24 | const expectedData = {
25 | subscriptions: {},
26 | collectionData: {},
27 | loginToken: undefined,
28 | };
29 | expectedData.subscriptions[pubName] = { '[]': true };
30 | expectedData.collectionData[collName] = [coll.find().fetch()];
31 |
32 | assert.deepEqual(context.getData(), expectedData);
33 | });
34 |
35 | it('subscribe with this.x apis', function () {
36 | const collName = Random.id();
37 | const coll = new Mongo.Collection(collName);
38 | coll.insert({ _id: 'one', age: 20 });
39 | coll.insert({ _id: 'two', age: 40 });
40 |
41 | const pubName = Random.id();
42 | Meteor.publish(pubName, function () {
43 | const data = coll.find().fetch();
44 | this.added(collName, data[0]._id, data[0]);
45 | this.added(collName, data[1]._id, data[1]);
46 | this.ready();
47 | });
48 |
49 | const context = new FastRender._Context();
50 | context.subscribe(pubName);
51 |
52 | const expectedData = {
53 | subscriptions: {},
54 | collectionData: {},
55 | loginToken: undefined,
56 | };
57 | expectedData.subscriptions[pubName] = { '[]': true };
58 | expectedData.collectionData[collName] = [coll.find().fetch()];
59 |
60 | assert.deepEqual(context.getData(), expectedData);
61 | });
62 |
63 | it('subscribe with this.x apis - no ready called', function () {
64 | const pubName = Random.id();
65 | Meteor.publish(pubName, function () { });
66 |
67 | const context = new FastRender._Context();
68 | context.subscribe(pubName);
69 |
70 | const expectedData = {
71 | subscriptions: {},
72 | collectionData: {},
73 | loginToken: undefined,
74 | };
75 |
76 | assert.deepEqual(context.getData(), expectedData);
77 | });
78 |
79 | it('logged in user', function (done) {
80 | const id = Random.id();
81 | const username = Random.id();
82 | const loginToken = Random.id();
83 |
84 | Meteor.users.insert({ _id: id, username: username });
85 | const hashedToken = Accounts._hashLoginToken(loginToken);
86 | Meteor.users.update(id, {
87 | $set: { 'services.resume.loginTokens.hashedToken': hashedToken },
88 | });
89 |
90 | const pubName = Random.id();
91 | Meteor.publish(pubName, function () {
92 | assert.equal(this.userId, id);
93 | assert.equal(Meteor.userId(), id);
94 | done();
95 | });
96 |
97 | const context = new FastRender._Context(loginToken);
98 | context.subscribe(pubName);
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/tests/server/index.js:
--------------------------------------------------------------------------------
1 | import './context';
2 | import './integration';
3 |
--------------------------------------------------------------------------------
/tests/server/integration.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | import { FastRender } from 'meteor/communitypackages:fast-render';
3 | import { InjectData } from 'meteor/communitypackages:inject-data';
4 | import { Random } from 'meteor/random';
5 | import { Meteor } from 'meteor/meteor';
6 | import { HTTP } from 'meteor/http';
7 | import { check } from 'meteor/check';
8 | import { assert } from 'chai';
9 | import { URL } from 'url';
10 |
11 | describe('integration', function () {
12 | it('a simple route', function () {
13 | const collName = Random.id();
14 | const pubName = Random.id();
15 | const path = '/' + Random.id();
16 | const obj = { _id: 'one', aa: 10 };
17 |
18 | const coll = new Meteor.Collection(collName);
19 | coll.insert(obj);
20 |
21 | Meteor.publish(pubName, function () {
22 | return coll.find();
23 | });
24 |
25 | FastRender.route(path, function () {
26 | this.subscribe(pubName);
27 | });
28 |
29 | const data = getFRData(path);
30 | assert.isDefined(data.subscriptions[pubName]);
31 | assert.deepEqual(data.collectionData[collName][0][0], obj);
32 | });
33 |
34 | it('onAllRoutes', function () {
35 | const collName = Random.id();
36 | const pubName = Random.id();
37 | const path = '/' + Random.id();
38 | const obj = { _id: 'one', aa: 10 };
39 |
40 | const coll = new Meteor.Collection(collName);
41 | coll.insert(obj);
42 |
43 | const cursorHandler = createCursorHandler(function () {
44 | return coll.find();
45 | });
46 |
47 | Meteor.publish(pubName, function () {
48 | return cursorHandler.get();
49 | });
50 | FastRender.route(path, function () { });
51 | FastRender.onAllRoutes(function () {
52 | this.subscribe(pubName);
53 | });
54 |
55 | const data = getFRData(path);
56 | assert.isDefined(data.subscriptions[pubName]);
57 | assert.deepEqual(data.collectionData[collName][0][0], obj);
58 | cursorHandler.stop();
59 | });
60 |
61 | it('onAllRoutes + route', function () {
62 | const collName = Random.id();
63 | const pubName = Random.id();
64 | const path = '/' + Random.id();
65 | const obj1 = { _id: 'one', aa: 10 };
66 | const obj2 = { _id: 'two', aa: 10 };
67 |
68 | const coll = new Meteor.Collection(collName);
69 | coll.insert(obj1);
70 | coll.insert(obj2);
71 |
72 | const cursorHandler = createCursorHandler(function (id) {
73 | return coll.find({ _id: id });
74 | });
75 |
76 | Meteor.publish(pubName, function (id) {
77 | check(id, String);
78 | return cursorHandler.get(id);
79 | });
80 |
81 | FastRender.onAllRoutes(function () {
82 | this.subscribe(pubName, 'one');
83 | });
84 |
85 | FastRender.route(path, function () {
86 | this.subscribe(pubName, 'two');
87 | });
88 |
89 | const data = getFRData(path);
90 | assert.isDefined(data.subscriptions[pubName]);
91 | assert.deepEqual(data.collectionData[collName][0][0], obj1);
92 | assert.deepEqual(data.collectionData[collName][1][0], obj2);
93 | cursorHandler.stop();
94 | });
95 |
96 | it('null publications', function () {
97 | const collName = Random.id();
98 | const path = '/' + Random.id();
99 | const obj = { _id: 'one', aa: 10 };
100 |
101 | const coll = new Meteor.Collection(collName);
102 | coll.insert(obj);
103 |
104 | const cursorHandler = createCursorHandler(function () {
105 | return coll.find();
106 | });
107 | Meteor.publish(null, function () {
108 | return cursorHandler.get();
109 | });
110 |
111 | FastRender.route(path, function () { });
112 |
113 | const data = getFRData(path);
114 | assert.deepEqual(data.collectionData[collName][0][0], obj);
115 | cursorHandler.stop();
116 | });
117 |
118 | it('send data via this.*apis', function () {
119 | const collName = Random.id();
120 | const pubName = Random.id();
121 | const path = '/' + Random.id();
122 | const obj = { _id: 'one', aa: 10 };
123 |
124 | Meteor.publish(pubName, function () {
125 | const sub = this;
126 | const { _id, ...newObj } = obj;
127 | sub.added(collName, _id, newObj);
128 | Meteor.setTimeout(function () {
129 | sub.ready();
130 | }, 100);
131 | });
132 |
133 | FastRender.route(path, function () {
134 | this.subscribe(pubName);
135 | });
136 |
137 | const data = getFRData(path);
138 | assert.isDefined(data.subscriptions[pubName]);
139 | assert.deepEqual(data.collectionData[collName][0][0], obj);
140 | });
141 |
142 | it('send delayed data via this.* apis', function () {
143 | const collName = Random.id();
144 | const pubName = Random.id();
145 | const path = '/' + Random.id();
146 | const obj = { _id: 'one', aa: 10 };
147 |
148 | Meteor.publish(pubName, function () {
149 | const sub = this;
150 | Meteor.setTimeout(function () {
151 | const { _id, ...newObj } = obj;
152 | sub.added(collName, _id, newObj);
153 | sub.ready();
154 | }, 1000);
155 | });
156 |
157 | FastRender.route(path, function () {
158 | this.subscribe(pubName);
159 | });
160 |
161 | const data = getFRData(path);
162 | assert.isUndefined(data.subscriptions[pubName]);
163 | assert.deepEqual(data.collectionData, {});
164 | });
165 |
166 | it('error inside a publication', function () {
167 | const collName = Random.id();
168 | const pubName = Random.id();
169 | const path = '/' + Random.id();
170 | const obj = { _id: 'one', aa: 10 };
171 |
172 | const coll = new Meteor.Collection(collName);
173 | coll.insert(obj);
174 |
175 | Meteor.publish(pubName, function () {
176 | throw new Error('some bad thing happens');
177 | });
178 |
179 | FastRender.route(path, function () {
180 | this.subscribe(pubName);
181 | });
182 |
183 | const data = getFRData(path);
184 | assert.deepEqual(data.collectionData, {});
185 | });
186 |
187 | it('error inside a null publication', function () {
188 | const collName = Random.id();
189 | const path = '/' + Random.id();
190 | const obj = { _id: 'one', aa: 10 };
191 |
192 | const coll = new Meteor.Collection(collName);
193 | coll.insert(obj);
194 |
195 | Meteor.publish(null, function () {
196 | throw new Error('some bad thing happens');
197 | });
198 |
199 | FastRender.route(path, function () { });
200 | const data = getFRData(path);
201 | assert.deepEqual(data.collectionData, {});
202 | });
203 | });
204 |
205 | it('when path has no leading slash', function () {
206 | const path = Random.id();
207 |
208 | assert.throws(function () {
209 | FastRender.route(path, function () { });
210 | }, 'Error: path (' + path + ') must begin with a leading slash "/"');
211 | });
212 |
213 | function getFRData (path) {
214 | const url = new URL(path, process.env.ROOT_URL).toString();
215 | const options = {
216 | headers: {
217 | Accept: 'text/html',
218 | },
219 | };
220 | const res = HTTP.get(url, options);
221 | const encodedData = res.content.match(/data">(.*)<\/script/)[1];
222 | return InjectData._decode(encodedData)['fast-render-data'];
223 | }
224 |
225 | function createCursorHandler (callback) {
226 | let stop = false;
227 | function getFn () {
228 | if (stop) {
229 | return [];
230 | } else {
231 | return callback.apply(this, arguments);
232 | }
233 | }
234 |
235 | function stopFn () {
236 | stop = true;
237 | }
238 |
239 | return {
240 | get: getFn,
241 | stop: stopFn,
242 | };
243 | }
244 |
--------------------------------------------------------------------------------