Repository: ionic-team/stencil-store
Branch: main
Commit: 4a5628ade709
Files: 52
Total size: 94.3 KB
Directory structure:
gitextract_14714bwu/
├── .github/
│ ├── CODEOWNERS
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ └── feature_request.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── dependabot.yml
│ ├── ionic-issue-bot.yml
│ └── workflows/
│ ├── build.yml
│ ├── main.yml
│ ├── publish-npm.yml
│ ├── release-dev.yml
│ ├── release-orchestrator.yml
│ └── release-production.yml
├── .gitignore
├── .nvmrc
├── .prettierrc.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── package.json
├── rollup.config.js
├── src/
│ ├── index.test.ts
│ ├── index.ts
│ ├── observable-map.test.ts
│ ├── observable-map.ts
│ ├── store.test.ts
│ ├── store.ts
│ ├── subscriptions/
│ │ ├── stencil.test.ts
│ │ └── stencil.ts
│ ├── types.ts
│ ├── utils.test.ts
│ └── utils.ts
├── test-app/
│ ├── .editorconfig
│ ├── .gitignore
│ ├── .prettierrc.json
│ ├── LICENSE
│ ├── package.json
│ ├── readme.md
│ ├── src/
│ │ ├── components/
│ │ │ ├── change-store/
│ │ │ │ └── change-store.tsx
│ │ │ ├── display-store/
│ │ │ │ ├── display-store.e2e.ts
│ │ │ │ └── display-store.tsx
│ │ │ └── simple-store/
│ │ │ ├── display-store.spec.ts
│ │ │ └── display-store.tsx
│ │ ├── components.d.ts
│ │ ├── index.html
│ │ ├── index.ts
│ │ └── utils/
│ │ └── greeting-store.ts
│ ├── stencil.config.ts
│ └── tsconfig.json
├── tsconfig.json
└── vitest.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CODEOWNERS
================================================
* @stenciljs/technical-steering-committee
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [johnjenkins]
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 🐛 Bug Report
description: Create a report to help us improve Stencil Store
title: 'bug: '
body:
- type: checkboxes
attributes:
label: Prerequisites
description: Please ensure you have completed all of the following.
options:
- label: I have read the [Contributing Guidelines](https://github.com/stenciljs/core/blob/main/.github/CONTRIBUTING.md).
required: true
- label: I agree to follow the [Code of Conduct](https://github.com/stenciljs/.github/blob/main/CODE_OF_CONDUCT.md).
required: true
- label: I have searched for [existing issues](https://github.com/stenciljs/store/issues) that already report this problem, without success.
required: true
- type: input
attributes:
label: Stencil Store Version
description: The version number of Stencil Store where the issue is occurring.
validations:
required: true
- type: input
attributes:
label: Stencil Version
description: The version number of Stencil where the issue is occurring.
validations:
required: true
- type: textarea
attributes:
label: Current Behavior
description: A clear description of what the bug is and how it manifests.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A clear description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: Please explain the steps required to duplicate this issue.
validations:
required: true
- type: input
attributes:
label: Code Reproduction URL
description: Please reproduce this issue in a blank Stencil starter application and provide a link to the repo. Run `npm init stencil` to quickly spin up a Stencil project. This is the best way to ensure this issue is triaged quickly. Issues without a code reproduction may be closed if the Stencil Team cannot reproduce the issue you are reporting.
placeholder: https://github.com/...
validations:
required: true
- type: textarea
attributes:
label: Additional Information
description: List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to fix, Stack Overflow links, forum links, etc.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
contact_links:
- name: 💻 Stencil
url: https://github.com/stenciljs/core/issues/new/choose
about: This issue tracker is not for Stencil compiler issues. Please file compiler issues on the Stencil repo.
- name: 📚 Documentation
url: https://github.com/stenciljs/site/issues/new/choose
about: This issue tracker is not for documentation issues. Please file documentation issues on the Stencil site repo.
- name: 📝 Create Stencil CLI
url: https://github.com/stenciljs/create-stencil/issues/new/choose
about: This issue tracker is not for Create Stencil CLI issues. Please file CLI issues on the Create Stencil CLI repo.
- name: 🤔 Support Question
url: https://forum.ionicframework.com/
about: This issue tracker is not for support questions. Please post your question on the Ionic Forums.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: 💡 Feature Request
description: Suggest an idea for Stencil Store
title: 'feat: '
body:
- type: checkboxes
attributes:
label: Prerequisites
description: Please ensure you have completed all of the following.
options:
- label: I have read the [Contributing Guidelines](https://github.com/stenciljs/core/blob/main/.github/CONTRIBUTING.md).
required: true
- label: I agree to follow the [Code of Conduct](https://github.com/stenciljs/.github/blob/main/CODE_OF_CONDUCT.md).
required: true
- label: I have searched for [existing issues](https://github.com/stenciljs/store/issues) that already include this feature request, without success.
required: true
- type: textarea
attributes:
label: Describe the Feature Request
description: A clear and concise description of what the feature does.
validations:
required: true
- type: textarea
attributes:
label: Describe the Use Case
description: A clear and concise use case for what problem this feature would solve.
validations:
required: true
- type: textarea
attributes:
label: Describe Preferred Solution
description: A clear and concise description of how you want this feature to be added to Stencil Store.
- type: textarea
attributes:
label: Describe Alternatives
description: A clear and concise description of any alternative solutions or features you have considered.
- type: textarea
attributes:
label: Related Code
description: If you are able to illustrate the feature request with an example, please provide a sample Stencil component(s). Run `npm init stencil` to quickly spin up a Stencil project.
- type: textarea
attributes:
label: Additional Information
description: List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to implement, Stack Overflow links, forum links, etc.
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!-- Please refer to our contributing documentation for any questions on submitting a pull request, or let us know here if you need any help: https://github.com/stenciljs/core/blob/main/.github/CONTRIBUTING.md -->
## Pull request checklist
Please check if your PR fulfills the following requirements:
- [ ] Docs have been reviewed and added / updated if needed (for bug fixes / features)
- [ ] Build (`npm run build`) was run locally and any changes were pushed
- [ ] Tests (`npm test`) were run locally and passed
- [ ] Prettier (`npm run prettier`) was run locally and passed
## Pull request type
<!-- Please do not submit updates to dependencies unless it fixes an issue. -->
<!-- Please try to limit your pull request to one type, submit multiple pull requests if needed. -->
Please check the type of change your PR introduces:
- [ ] Bugfix
- [ ] Feature
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] Documentation content changes
- [ ] Other (please describe):
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
GitHub Issue Number: N/A
## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by this PR. -->
-
-
-
## Does this introduce a breaking change?
- [ ] Yes
- [ ] No
<!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. -->
## Testing
<!-- Please describe the steps you took to test the changes in this PR. These steps can be programmatic (e.g. unit tests) and/or manual. -->
## Other information
<!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. -->
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
groups:
patch-deps-updates-main:
update-types:
- "patch"
minor-deps-updates-main:
update-types:
- "minor"
major-deps-updates-main:
update-types:
- "major"
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
time: "11:00"
open-pull-requests-limit: 10
groups:
patch-deps-updates:
update-types:
- "patch"
minor-deps-updates:
update-types:
- "minor"
major-deps-updates:
update-types:
- "major"
================================================
FILE: .github/ionic-issue-bot.yml
================================================
triage:
label: triage
dryRun: false
closeAndLock:
labels:
- label: "ionitron: support"
message: >
Thanks for the issue! This issue appears to be a support request. We use this issue tracker exclusively for
bug reports and feature requests. Please use our [discord channel](https://chat.stenciljs.com/)
for questions about Stencil Store.
Thank you for using Stencil Store!
- label: "ionitron: missing template"
message: >
Thanks for the issue! It appears that you have not filled out the provided issue template. We use this issue
template in order to gather more information and further assist you. Please create a new issue and ensure the
template is fully filled out.
Thank you for using Stencil Store!
close: true
lock: true
dryRun: false
comment:
labels:
- label: "ionitron: needs reproduction"
message: >
Thanks for the issue! This issue has been labeled as `needs reproduction`. This label is added to issues that
need a code reproduction.
Please reproduce this issue in an Stencil starter component library and Stencil Store. Please provide a way for
us to access it (GitHub repo, StackBlitz, etc). Without a reliable code reproduction, it is unlikely we will be
able to resolve the issue, leading to it being closed.
If you have already provided a code snippet and are seeing this message, it is likely that the code snippet was
not enough for our team to reproduce the issue.
For a guide on how to create a good reproduction, see our
[Contributing Guide](https://github.com/stenciljs/core/blob/main/.github/CONTRIBUTING.md).
dryRun: false
noReply:
maxIssuesPerRun: 100
includePullRequests: false
label: Awaiting Reply
close: false
lock: false
dryRun: false
noReproduction:
days: 14
maxIssuesPerRun: 100
label: "ionitron: needs reproduction"
responseLabel: triage
exemptProjects: true
exemptMilestones: true
message: >
Thanks for the issue! This issue is being closed due to the lack of a code reproduction. If this is still
an issue with the latest version of Stencil & Stencil Store, please create a new issue and ensure the template is
fully filled out.
Thank you for using Stencil Store!
close: true
lock: true
dryRun: false
stale:
days: 30
maxIssuesPerRun: 100
exemptLabels:
- "Bug: Validated"
- "Feature: Want this? Upvote it!"
- good first issue
- help wanted
- Reply Received
- Request For Comments
- "Resolution: Needs Investigation"
- "Resolution: Refine"
- triage
exemptAssigned: true
exemptProjects: true
exemptMilestones: true
label: "ionitron: stale issue"
message: >
Thanks for the issue! This issue is being closed due to inactivity. If this is still
an issue with the latest version of Stencil, please create a new issue and ensure the
template is fully filled out.
Thank you for using Stencil Store!
close: true
lock: true
dryRun: false
================================================
FILE: .github/workflows/build.yml
================================================
name: Build Stencil Store
on:
workflow_call:
# Make this a reusable workflow, no value needed
# https://docs.github.com/en/actions/using-workflows/reusing-workflows
jobs:
build_stencil_store:
name: Build Stencil Store
runs-on: 'ubuntu-latest'
steps:
- name: 📥 Checkout Code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# the pull_request_target event will consider the HEAD of `main` to be the SHA to use.
# attempt to use the SHA associated with a pull request and fallback to HEAD of `main`
ref: ${{ github.event_name == 'pull_request_target' && format('refs/pull/{0}/merge', github.event.number) || '' }}
persist-credentials: false
- name: 🕸️ Get Core Dependencies
uses: stenciljs/.github/actions/get-core-dependencies@main
- name: 📦 Stencil Store Build
run: npm run build
shell: bash
- name: 🧪 Unit Tests
run: npm test
shell: bash
- name: 📤 Upload Build Artifacts
uses: stenciljs/.github/actions/upload-archive@main
================================================
FILE: .github/workflows/main.yml
================================================
name: CI
on:
push:
branches:
- 'main'
pull_request:
branches:
- '**'
jobs:
build:
name: (build stencil ${{ matrix.stencil_version }})
strategy:
fail-fast: false
matrix:
# Run with multiple different versions of Stencil in parallel:
# 1. DEFAULT - uses the version of Stencil written in `package-lock.json`, keeping the same version used by the
# Stencil team as a source of truth
# 2. 2 - will install the latest release under major version 2 of Stencil. This should be kept as long as this
# library supports Stencil v2.Y.Z
# 3. 3 - will install the latest release under major version 3 of Stencil. This should be kept as long as this
# library supports Stencil v3.Y.Z
# 4. 4 - will install the latest release under major version 4 of Stencil. This should be kept as long as this
# library supports Stencil v4.Y.Z
stencil_version: ['DEFAULT', '2', '3', '4']
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout Code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: 🕸️ Get Core Dependencies
uses: stenciljs/.github/actions/get-core-dependencies@main
- name: 🧪 Test
run: npm run test
- name: 📦 Install Stencil ${{matrix.stencil_version}}
run: npm install --save-dev @stencil/core@${{matrix.stencil_version}}
shell: bash
if: ${{ matrix.stencil_version != 'DEFAULT' }}
- name: 📊 Report Stencil Version
run: npm ls @stencil/core
shell: bash
- name: 🏗️ Build
run: npm run build
shell: bash
- name: 🔗 Create Symlink
run: npm link
shell: bash
- name: 📂 Enter test-app Directory
run: cd test-app
shell: bash
- name: 📦 Install test-app Dependencies
run: npm install
shell: bash
- name: 🔗 Link Stencil Store
run: npm link @stencil/store
- name: 🏗️ Build test-app
run: npm run build
shell: bash
- name: 🧪 Test test-app
run: npm test
shell: bash
================================================
FILE: .github/workflows/publish-npm.yml
================================================
name: 'Publish Stencil Store'
on:
workflow_call:
inputs:
version:
description: 'Version or semver bump to publish.'
required: true
type: string
tag:
description: 'npm dist-tag to publish under.'
required: true
type: string
node-version:
description: 'Node.js version used for publishing.'
required: false
type: string
default: '20'
registry-url:
description: 'npm registry URL.'
required: false
type: string
default: 'https://registry.npmjs.org'
scope:
description: 'npm scope that should use trusted publisher auth.'
required: false
type: string
default: '@stencil'
package-directory:
description: 'Directory that contains the package to publish.'
required: false
type: string
default: '.'
install-command:
description: 'Command used to install dependencies. Leave blank to skip.'
required: false
type: string
default: 'npm ci'
install-working-directory:
description: 'Working directory for the install command.'
required: false
type: string
default: '.'
build-command:
description: 'Command used to build the package. Leave blank to skip.'
required: false
type: string
default: 'npm run build'
build-working-directory:
description: 'Working directory for the build command.'
required: false
type: string
default: '.'
permissions:
contents: write
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout Code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: 🟢 Configure Node for Publish
if: ${{ inputs.scope == '' }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ inputs.node-version }}
registry-url: ${{ inputs.registry-url }}
- name: 🟢 Configure Node for Publish (Scoped)
if: ${{ inputs.scope != '' }}
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ inputs.node-version }}
registry-url: ${{ inputs.registry-url }}
scope: ${{ inputs.scope }}
- name: 🔄 Ensure Latest npm
run: npm install -g npm@latest
shell: bash
- name: 📦 Install Dependencies
if: ${{ inputs.install-command != '' }}
run: ${{ inputs.install-command }}
shell: bash
working-directory: ${{ inputs.install-working-directory }}
- name: 🛠️ Build Package
if: ${{ inputs.build-command != '' }}
run: ${{ inputs.build-command }}
shell: bash
working-directory: ${{ inputs.build-working-directory }}
- name: 🏷️ Set Version
run: npm version --no-git-tag-version --allow-same-version ${{ inputs.version }}
shell: bash
working-directory: ${{ inputs.package-directory }}
- name: 🔎 Get Version from package.json
id: get-version
run: |
# Normalize package directory path to prevent path traversal issues
PKG_DIR="${{ inputs.package-directory }}"
if [ "$PKG_DIR" = "." ] || [ -z "$PKG_DIR" ]; then
PKG_DIR="."
fi
# Use path.resolve to safely construct the absolute path
VERSION=$(node -e "const path = require('path'); const pkgPath = path.resolve('$PKG_DIR', 'package.json'); const pkg = require(pkgPath); console.log(pkg.version);")
if [ -z "$VERSION" ]; then
echo "❌ Failed to extract version from $PKG_DIR/package.json. Ensure the file exists and contains a valid 'version' field." >&2
exit 1
fi
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Extracted version: $VERSION from $PKG_DIR/package.json"
shell: bash
- name: 📝 Commit Version Changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if [ "${{ inputs.package-directory }}" = "." ]; then
git add package.json package-lock.json
else
git add ${{ inputs.package-directory }}/package.json ${{ inputs.package-directory }}/package-lock.json 2>/dev/null || true
fi
# Only commit if there are changes
if ! git diff --staged --quiet; then
git commit -m "chore: bump version to ${{ steps.get-version.outputs.VERSION }}"
else
echo "No changes to commit"
fi
shell: bash
- name: 🏷️ Create Git Tag
# Skip tag creation for dev builds to avoid cluttering the repository
if: ${{ inputs.tag != 'dev' }}
run: |
TAG_NAME="v${{ steps.get-version.outputs.VERSION }}"
echo "Checking for existing tag: $TAG_NAME"
# Check if tag exists locally
if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then
echo "Tag $TAG_NAME already exists locally, skipping creation"
exit 0
fi
# Check if tag exists remotely
if git ls-remote --tags origin "$TAG_NAME" | grep -q "$TAG_NAME"; then
echo "Tag $TAG_NAME already exists on remote, skipping creation"
exit 0
fi
# Create the tag
echo "Creating annotated tag $TAG_NAME"
git tag -a "$TAG_NAME" -m "Release $TAG_NAME"
echo "✓ Tag $TAG_NAME created successfully"
shell: bash
- name: 📤 Push Changes and Tag
run: |
# Push the commit to the current branch
git push origin HEAD
# Push the tag (only for non-dev builds)
if [ "${{ inputs.tag }}" != "dev" ]; then
git push origin "v${{ steps.get-version.outputs.VERSION }}"
else
echo "Skipping tag push for dev build"
fi
shell: bash
- name: 🚀 Publish to npm
run: npm publish --tag ${{ inputs.tag }} --provenance
shell: bash
working-directory: ${{ inputs.package-directory }}
================================================
FILE: .github/workflows/release-dev.yml
================================================
name: 'Stencil Store Dev Release'
on:
workflow_call:
permissions:
contents: write
id-token: write
jobs:
build_stencil_store:
name: 🏗️ Build Stencil Store
uses: ./.github/workflows/build.yml
get_dev_version:
name: 🔍 Get Dev Build Version
runs-on: ubuntu-latest
needs: build_stencil_store
outputs:
dev-version: ${{ steps.generate-dev-version.outputs.DEV_VERSION }}
steps:
- name: 📥 Checkout Code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: ⚙️ Generate Dev Version
id: generate-dev-version
run: |
PKG_JSON_VERSION=$(cat package.json | jq -r '.version')
GIT_HASH=$(git rev-parse --short HEAD)
DEV_VERSION=$PKG_JSON_VERSION-dev.$(date +"%s").$GIT_HASH
echo "DEV_VERSION=$DEV_VERSION" >> $GITHUB_OUTPUT
shell: bash
publish_dev:
name: 🚀 Publish Dev Build
needs: get_dev_version
uses: ./.github/workflows/publish-npm.yml
with:
tag: dev
version: ${{ needs.get_dev_version.outputs.dev-version }}
================================================
FILE: .github/workflows/release-orchestrator.yml
================================================
name: 'Release - Stencil Store'
on:
workflow_dispatch:
inputs:
channel:
description: 'Which release workflow should run?'
required: true
type: choice
default: dev
options:
- dev
- production
bump:
description: 'Semver bump for production releases.'
required: false
type: choice
default: patch
options:
- patch
- minor
- major
permissions:
contents: read
id-token: write
jobs:
run-dev:
if: ${{ inputs.channel == 'dev' }}
uses: ./.github/workflows/release-dev.yml
secrets: inherit
run-production:
if: ${{ inputs.channel == 'production' }}
uses: ./.github/workflows/release-production.yml
secrets: inherit
with:
bump: ${{ inputs.bump }}
================================================
FILE: .github/workflows/release-production.yml
================================================
name: 'Stencil Store Production Release'
on:
workflow_call:
inputs:
bump:
description: 'Semver bump to apply (patch, minor, major).'
required: true
type: string
default: patch
permissions:
contents: write
id-token: write
jobs:
validate_bump:
name: ✅ Validate Bump Input
runs-on: ubuntu-latest
steps:
- name: 🔎 Ensure bump is allowed
env:
BUMP: ${{ inputs.bump }}
run: |
case "$BUMP" in
patch|minor|major)
exit 0
;;
*)
echo "::error::Invalid bump input: '$BUMP'. Allowed values: patch, minor, major."
exit 1
;;
esac
build_stencil_store:
name: 🏗️ Build Stencil Store
uses: ./.github/workflows/build.yml
publish_production:
name: 🚀 Publish Production Build
needs:
- build_stencil_store
- validate_bump
uses: ./.github/workflows/publish-npm.yml
with:
tag: latest
version: ${{ inputs.bump }}
================================================
FILE: .gitignore
================================================
dist/
www/
loader/
build/
*~
*.sw[mnpcod]
*.log
*.lock
*.tmp
*.tmp.*
log.txt
*.sublime-project
*.sublime-workspace
*.tgz
.stencil/
.idea/
.vscode/
.sass-cache/
.versions/
node_modules/
$RECYCLE.BIN/
.DS_Store
Thumbs.db
UserInterfaceState.xcuserstate
.env
coverage
================================================
FILE: .nvmrc
================================================
v22.14.0
================================================
FILE: .prettierrc.json
================================================
{
"parser": "typescript",
"printWidth": 100,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
}
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to @stencil/store
Thank you for your interest in contributing to @stencil/store! This document provides guidelines and information for contributors.
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [Getting Started](#getting-started)
- [Development Setup](#development-setup)
- [Making Changes](#making-changes)
- [Testing](#testing)
- [Pull Request Process](#pull-request-process)
- [Release Process](#release-process)
## Code of Conduct
This project follows the same code of conduct as the main StencilJS project. Please be respectful and inclusive in all interactions.
## Getting Started
1. Fork the repository on GitHub
2. Clone your fork locally:
```bash
git clone https://github.com/YOUR_USERNAME/store.git
cd store
```
3. Add the upstream repository:
```bash
git remote add upstream https://github.com/stenciljs/store.git
```
## Development Setup
### Prerequisites
- Node.js >= 22.14.0
- npm >= 9.x
### Installation
```bash
npm install
```
### Available Scripts
- `npm run build` - Build the project
- `npm test` - Run all tests
- `npm run test.unit` - Run unit tests with Vitest
- `npm run test.prettier` - Check code formatting
- `npm run prettier` - Format code with Prettier
### Project Structure
```
src/
├── index.ts # Main entry point
├── store.ts # Core store implementation
├── observable-map.ts # Observable map utilities
├── types.ts # TypeScript type definitions
├── utils.ts # Utility functions
└── subscriptions/
└── stencil.ts # Stencil-specific subscriptions
```
## Making Changes
1. Create a new branch from `main`:
```bash
git checkout -b feature/your-feature-name
```
2. Make your changes following these guidelines:
- Write clear, readable code
- Add tests for new functionality
- Update documentation if needed
- Follow the existing code style
3. Run tests to ensure everything works:
```bash
npm test
```
4. Commit your changes with a clear message:
```bash
git commit -m "feat: add new feature description"
```
### Commit Message Guidelines
Use conventional commit format:
- `feat:` - New features
- `fix:` - Bug fixes
- `docs:` - Documentation changes
- `style:` - Code style changes (formatting, etc.)
- `refactor:` - Code refactoring
- `test:` - Adding or updating tests
- `chore:` - Build process or auxiliary tool changes
## Testing
### Unit Tests
The project uses Vitest for unit testing. Tests are located alongside the source files with `.test.ts` extensions.
```bash
npm run test.unit
```
### Test App
There's a test application in the `test-app/` directory that demonstrates the store functionality:
```bash
cd test-app
npm install
npm start
```
### Writing Tests
When adding new features or fixing bugs:
1. Add unit tests in the appropriate `.test.ts` file
2. Test both positive and negative cases
3. Include edge cases
4. Ensure tests are isolated and don't depend on external state
## Pull Request Process
1. Push your branch to your fork:
```bash
git push origin feature/your-feature-name
```
2. Create a Pull Request on GitHub with:
- Clear title and description
- Reference any related issues
- Include tests for new functionality
- Update documentation if needed
3. Ensure all checks pass:
- All tests must pass
- Code must be properly formatted
- No linting errors
4. Address any review feedback
5. Once approved, a maintainer will merge your PR
## Release Process
### Release Types
We follow [Semantic Versioning (SemVer)](https://semver.org/) for releases. Choose the appropriate release type based on your changes:
#### Patch Release (x.x.X)
Use for:
- Bug fixes
- Documentation updates
- Internal refactoring that doesn't change the API
- Performance improvements without API changes
#### Minor Release (x.X.x)
Use for:
- New features that are backward compatible
- New API methods or options
- Deprecating functionality (without removing)
- Significant internal improvements
#### Major Release (X.x.x)
Use for:
- Breaking changes to the public API
- Removing deprecated functionality
- Changes that require users to modify their code
- Major architectural changes
### Development Releases
Development Releases (or "Dev Releases", "Dev Builds") are installable instances of Stencil Store that are:
- Published to the npm registry for distribution within and outside the Stencil team
- Built using the same infrastructure as production releases, with less safety checks
- Used to verify a fix or change to the project prior to a production release
#### How to Publish Dev Releases
Only members of the Stencil team may create dev builds of Stencil Store.
To publish a dev build:
1. Navigate to the [Stencil Store Release GitHub Action](https://github.com/stenciljs/store/actions/workflows/release.yml) in your browser.
2. Click the 'Run Workflow' dropdown on the right hand side of the page
3. Configure the workflow inputs:
- **Release type**: Select `patch`, `minor`, or `major` (this won't affect the dev build version)
- **Dev Release**: Select `yes` to create a dev build
4. Select the branch you want to build from
5. Click 'Run Workflow'
6. Allow the workflow to run. Upon completion, the output of the 'Publish Dev Build' action will report the published version string.
Following a successful run of the workflow, the package can be installed from the npm registry like any other package.
#### Dev Release Format
Dev Builds are published to the NPM registry under the `@stencil/store` scope.
Unlike production builds, dev builds use a specially formatted version string to express its origins.
Dev builds follow the format `BASE_VERSION-dev.EPOCH_DATE.SHA`, where:
- `BASE_VERSION` is the latest production release changes to the build were based off of
- `EPOCH_DATE` is the number of seconds since January 1st, 1970 in UTC
- `SHA` is the git short SHA of the commit used in the release
As an example: `2.1.0-dev.1677185104.7c87e34` was built:
- With v2.1.0 as the latest production build at the time of the dev build
- On Fri, 26 Jan 2024 13:48:17 UTC
- With the commit `7c87e34`
### Production Releases
Only members of the Stencil team may create production releases of Stencil Store.
#### How to Publish Production Releases
1. Navigate to the [Stencil Store Release GitHub Action](https://github.com/stenciljs/store/actions/workflows/release.yml) in your browser.
2. Click the 'Run Workflow' dropdown on the right hand side of the page
3. Configure the workflow inputs:
- **Release type**: Select the appropriate type (`patch`, `minor`, or `major`) based on your changes
- **Dev Release**: Select `no` for production releases
4. Select the `main` branch (production releases should only be made from main)
5. Click 'Run Workflow'
6. The workflow will:
- Build and test the package
- Bump the version according to the selected release type
- Create a Git tag
- Publish to NPM with the `latest` tag
- Create a GitHub release
#### Release Checklist
Before creating a production release:
- [ ] Ensure all intended changes are merged to `main`
- [ ] All CI checks are passing
- [ ] Update any relevant documentation
- [ ] Consider if this should be a dev release first for testing
- [ ] Choose the correct release type (patch/minor/major)
- [ ] Notify the team of the planned release
## Questions or Issues?
- Create an issue on GitHub for bugs or feature requests
- Check existing issues before creating new ones
- Provide detailed information including steps to reproduce for bugs
Thank you for contributing to @stencil/store! 🎉
================================================
FILE: LICENSE
================================================
Copyright 2015-present Drifty Co.
http://drifty.com/
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# @stencil/store
Store is a lightweight shared state library by the [StencilJS](https://stenciljs.com/) core team. It implements a simple key/value map that efficiently re-renders components when necessary.
**Highlights:**
- 🪶 Lightweight
- ⚡ Zero dependencies
- 📦 Simple API, like a reactive Map
- 🚀 Best performance
## Installation
```
npm install @stencil/store --save-dev
```
## Example
**store.ts:**
```ts
import { createStore } from "@stencil/store";
const { state, onChange } = createStore({
clicks: 0,
seconds: 0,
squaredClicks: 0
});
onChange('clicks', value => {
state.squaredClicks = value ** 2;
});
export default state;
```
**component.tsx:**
```tsx
import { Component, h } from '@stencil/core';
import state from '../store';
@Component({
tag: 'app-profile',
})
export class AppProfile {
componentWillLoad() {
setInterval(() => state.seconds++, 1000);
}
render() {
return (
<div>
<p>
<MyGlobalCounter />
<p>
Seconds: {state.seconds}
<br />
Squared Clicks: {state.squaredClicks}
</p>
</p>
</div>
);
}
}
const MyGlobalCounter = () => {
return (
<button onClick={() => state.clicks++}>
{state.clicks}
</button>
);
};
```
## API
### `createStore<T>(initialState?: T | (() => T), shouldUpdate?)`
Create a new store with the given initial state. The type is inferred from `initialState`, or can be passed as the generic type `T`.
`initialState` can be a function that returns the actual initial state. This feature is just in case you have deep objects that mutate
as otherwise we cannot keep track of those.
```ts
const { reset, state } = createStore(() => ({
pageA: {
count: 1
},
pageB: {
count: 1
}
}));
state.pageA.count = 2;
state.pageB.count = 3;
reset();
state.pageA.count; // 1
state.pageB.count; // 1
```
Please, bear in mind that the object needs to be created inside the function, not just referenced. The following example won't work
as you might want it to, as the returned object is always the same one.
```ts
const object = {
pageA: {
count: 1
},
pageB: {
count: 1
}
};
const { reset, state } = createStore(() => object);
state.pageA.count = 2;
state.pageB.count = 3;
reset();
state.pageA.count; // 2
state.pageB.count; // 3
```
By default, store performs a exact comparison (`===`) between the new value, and the previous one in order to prevent unnecessary rerenders, however, this behaviour can be changed by providing a `shouldUpdate` function through the second argument. When this function returns `false`, the value won't be updated. By providing a custom `shouldUpdate()` function, applications can create their own fine-grained change detection logic, beyond the default `===`. This may be useful for certain use-cases to avoid any expensive re-rendering.
```ts
const shouldUpdate = (newValue, oldValue, propChanged) => {
return JSON.stringify(newValue) !== JSON.stringify(oldValue);
}
```
Returns a `store` object with the following properties.
#### `store.state`
The state object is proxied, i. e. you can directly get and set properties. If you access the state object in the `render` function of your component, Store will automatically re-render it when the state object is changed.
Note: [`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) objects are not supported by IE11 (not even with a polyfill), so you need to use the `store.get` and `store.set` methods of the API if you wish to support IE11.
#### `store.on(event, listener)`
Add a listener to the store for a certain action.
#### `store.onChange(propName, listener)`
Add a listener that is called when a specific property changes (either from a `set` or `reset`).
#### `store.get(propName)`
Get a property's value from the store.
#### `store.set(propName, value)`
Set a property's value in the store.
#### `store.reset()`
Reset the store to its initial state.
#### `store.use(...subscriptions)`
Use the given subscriptions in the store. A subscription is an object that defines one or more of the properties `get`, `set` or `reset`.
```ts
const { reset, state, use } = createStore({ a: 1, b: 2});
const unlog = use({
get: (key) => {
console.log(`Someone's reading prop ${key}`);
},
set: (key, newValue, oldValue) => {
console.log(`Prop ${key} changed from ${oldValue} to ${newValue}`);
},
reset: () => {
console.log('Store got reset');
},
dispose: () => {
console.log('Store got disposed');
},
})
state.a; // Someone's reading prop a
state.b = 3; // Prop b changed from 2 to 3
reset(); // Store got reset
unlog();
state.a; // Nothing is logged
state.b = 5; // Nothing is logged
reset(); // Nothing is logged
```
#### `store.dispose()`
Resets the store and all the internal state of the store that should not survive between tests.
## Testing
Like any global state library, state should be `dispose`d between each spec test.
Use the `dispose()` API in the `beforeEach` hook.
```ts
import store from '../store';
beforeEach(() => {
store.dispose();
});
```
================================================
FILE: package.json
================================================
{
"name": "@stencil/store",
"author": "StencilJS Team",
"version": "2.2.2",
"description": "Store is a lightweight shared state library by the StencilJS core team. Implements a simple key/value map that efficiently re-renders components when necessary.",
"license": "MIT",
"homepage": "https://stenciljs.com/docs/stencil-store",
"repository": {
"type": "git",
"url": "git://github.com/stenciljs/store.git"
},
"keywords": [
"stencil",
"redux",
"global",
"state",
"tunnel",
"hooks"
],
"type": "module",
"module": "dist/index.js",
"main": "dist/index.cjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"engines": {
"node": ">=18.0.0",
"npm": ">=6.0.0"
},
"scripts": {
"build": "run-s build.clean build.rollup",
"build.clean": "rimraf dist",
"build.rollup": "rollup -c rollup.config.js",
"prettier": "npm run prettier.base -- --write",
"prettier.base": "prettier --cache \"src/**/*.ts\"",
"prettier.dry-run": "npm run prettier.base -- --list-different",
"release": "np",
"test": "run-s test.*",
"test.prettier": "npm run prettier.dry-run",
"test.unit": "vitest",
"version": "npm run build"
},
"files": [
"dist"
],
"peerDependencies": {
"@stencil/core": ">=2.0.0 || >=3.0.0 || >= 4.0.0-beta.0 || >= 4.0.0"
},
"devDependencies": {
"@ionic/prettier-config": "^4.0.0",
"@rollup/plugin-typescript": "^12.3.0",
"@stencil/core": "^4.38.2",
"@types/node": "^25.0.2",
"@vitest/coverage-v8": "^4.0.5",
"np": "^11.0.1",
"npm-run-all2": "^8.0.4",
"prettier": "^3.6.2",
"rimraf": "^6.0.1",
"rollup": "^4.52.5",
"typescript": "~6.0.2",
"vitest": "^4.0.5"
},
"prettier": "@ionic/prettier-config"
}
================================================
FILE: rollup.config.js
================================================
import typescript from '@rollup/plugin-typescript';
import pkg from './package.json' with { type: 'json' };
export default {
input: 'src/index.ts',
output: [
{
format: 'cjs',
file: pkg.main
},
{
format: 'esm',
file: pkg.module
},
],
external: ['@stencil/core'],
plugins: [typescript({
outDir: 'dist'
})],
};
================================================
FILE: src/index.test.ts
================================================
import { describe, it, expect } from 'vitest';
import { createStore, createObservableMap } from './index';
describe('store', () => {
it('exports createStore and createObservableMap', () => {
expect(createStore).toBeDefined();
expect(createObservableMap).toBeDefined();
});
});
================================================
FILE: src/index.ts
================================================
export { createStore } from './store';
export { createObservableMap } from './observable-map';
// Types
export { ObservableMap, Subscription } from './types';
================================================
FILE: src/observable-map.test.ts
================================================
import { describe, expect, test, vi } from 'vitest';
import { createObservableMap } from './observable-map';
describe.each([
['reset', 'reset'],
['dispose calls reset', 'dispose'],
] as [string, 'reset' | 'dispose'][])('%s', (_, methodName) => {
test('returns all variable to their original state', () => {
const { [methodName]: method, state } = createObservableMap({
hola: 'hola',
name: 'Sergio',
});
state.hola = 'hello';
state.name = 'Manu';
expect(state.hola).toBe('hello');
expect(state.name).toBe('Manu');
method();
expect(state.hola).toBe('hola');
expect(state.name).toBe('Sergio');
});
test('extra properties get removed', () => {
const { [methodName]: method, state } = createObservableMap<Record<string, string>>({});
state['hola'] = 'hello';
expect(state).toHaveProperty('hola');
expect(state['hola']).toBe('hello');
method();
expect('hola' in state).toBe(false);
});
test('calls on', () => {
const { [methodName]: method, on } = createObservableMap({ hola: 'hello' });
const subscription = vi.fn();
on('reset', subscription);
method();
expect(subscription).toHaveBeenCalledTimes(1);
});
});
test('falls back to plain objects when Proxy is unavailable', () => {
const originalProxy = globalThis.Proxy;
// @ts-expect-error - simulating environments without Proxy support
globalThis.Proxy = undefined;
try {
const store = createObservableMap({ count: 1 });
store.set('count', 2);
store.set('extra', 3);
expect(store.get('count')).toBe(2);
expect('extra' in store.state).toBe(true);
store.reset();
expect(store.get('count')).toBe(1);
expect('extra' in store.state).toBe(false);
} finally {
globalThis.Proxy = originalProxy;
}
});
test('reset restores empty state when no default state is provided', () => {
const store = createObservableMap<Record<string, number>>();
store.set('num', 1);
expect(store.get('num')).toBe(1);
store.reset();
expect(store.get('num')).toBeUndefined();
});
describe('dispose', () => {
test('calls on', () => {
const { dispose, on } = createObservableMap({ hola: 'hello' });
const subscription = vi.fn();
on('dispose', subscription);
dispose();
expect(subscription).toHaveBeenCalledTimes(1);
});
});
describe.each([
['proxy', (state, _get, property) => state[property]],
['get fn', (_state, get, property) => get(property)],
] as [string, <T, K extends keyof T>(s: T, get: (prop: K) => T[K], prop: K) => T[K]][])('get (%s)', (_, getter) => {
test('returns the value for the property in the store', () => {
const { get, state } = createObservableMap({
hola: 'hello',
});
expect(getter(state, get, 'hola')).toBe('hello');
});
test('returns the modified value after being set', () => {
const { get, state } = createObservableMap({
hola: 'hello',
});
state.hola = 'ola';
expect(getter(state, get, 'hola')).toBe('ola');
});
test('calls on', () => {
const { get, on, state } = createObservableMap({
hola: 'hello',
});
const subscription = vi.fn();
on('get', subscription);
getter(state, get, 'hola');
expect(subscription).toHaveBeenCalledWith('hola');
});
});
describe.each([
['proxy', (state, _set, prop, value) => (state[prop] = value)],
['set fn', (_state, set, prop, value) => set(prop, value)],
] as [string, <T, K extends keyof T>(s: T, set: (prop: K, value: T[K]) => void, prop: K, value: T[K]) => void][])(
'set (%s)',
(_, setter) => {
test('sets the value for a property', () => {
const { set, state } = createObservableMap({
hola: 'hello',
});
setter(state, set, 'hola', 'ola');
expect(state.hola).toBe('ola');
});
test('calls on', () => {
const { set, on, state } = createObservableMap({
hola: 'hello',
});
const subscription = vi.fn();
on('set', subscription);
setter(state, set, 'hola', 'ola');
expect(subscription).toHaveBeenCalledWith('hola', 'ola', 'hello');
});
test('calls onChange', () => {
const { set, onChange, state } = createObservableMap({
hola: 'hello',
});
const subscription = vi.fn();
onChange('hola', subscription);
setter(state, set, 'hola', 'ola');
expect(subscription).toHaveBeenCalledWith('ola');
});
test('enumerable keys', () => {
const { state } = createObservableMap<any>({});
expect(Object.keys(state)).toEqual([]);
state.hello = 'hola';
expect(Object.keys(state)).toEqual(['hello']);
expect(Object.getOwnPropertyNames(state)).toEqual(['hello']);
const copy = { ...state };
expect(copy).toEqual({ hello: 'hola' });
});
test('in operator', () => {
const { state } = createObservableMap<any>({});
expect('hello' in state).toBe(false);
state.hello = 'hola';
expect('hello' in state).toBe(true);
});
},
);
describe('using a function as initial value', () => {
test('function gets invoked', () => {
const fn = vi.fn().mockReturnValue({ a: 1 });
createObservableMap(fn);
// We should not need to call this more than once
// when creating the proxy.
expect(fn).toHaveBeenCalledTimes(1);
});
test('returned value is used as object', () => {
const fn = vi.fn().mockReturnValue({ a: 1 });
const { state } = createObservableMap(fn);
expect(state).toHaveProperty('a', 1);
});
test('resetting resets deep objects', () => {
const fn = () => ({
a: {
b: 1,
c: 2,
},
d: [1, 2],
});
const { reset, state } = createObservableMap(fn);
state.a.b = 3;
state.a.c = 5;
state.d.push(1, 2);
reset();
expect(state.a).toHaveProperty('b', 1);
expect(state.a).toHaveProperty('c', 2);
expect(state).toHaveProperty('d', [1, 2]);
});
test('if the function does not create a new object, there is nothing we can do', () => {
const object = {
a: {
b: 1,
c: 2,
},
d: [1, 2],
};
const fn = () => object;
const { reset, state } = createObservableMap(fn);
state.a.b = 3;
state.a.c = 5;
state.d.push(1, 2);
reset();
expect(state.a).toHaveProperty('b', 3);
expect(state.a).toHaveProperty('c', 5);
expect(state).toHaveProperty('d', [1, 2, 1, 2]);
});
});
test('unregister events', () => {
const { reset, state, on, onChange } = createObservableMap({
hola: 'hola',
name: 'Sergio',
});
const SET = vi.fn();
const GET = vi.fn();
const RESET = vi.fn();
const CHANGE = vi.fn();
const unset = on('set', SET);
const unget = on('get', GET);
const unreset = on('reset', RESET);
const unChange = onChange('hola', CHANGE);
state.hola = 'hola2';
state.name = 'hola2';
expect(SET).toHaveBeenCalledTimes(2);
unset();
state.hola = 'hola3';
expect(SET).toHaveBeenCalledTimes(2);
state.hola;
state.name;
expect(GET).toHaveBeenCalledTimes(2);
unget();
state.name;
expect(GET).toHaveBeenCalledTimes(2);
reset();
reset();
expect(RESET).toHaveBeenCalledTimes(2);
unreset();
reset();
expect(RESET).toHaveBeenCalledTimes(2);
expect(CHANGE).toHaveBeenCalledTimes(5);
unChange();
reset();
state.hola = 'hola';
expect(CHANGE).toHaveBeenCalledTimes(5);
});
test('default change detector', () => {
const store = createObservableMap({
str: 'hola',
});
const SET = vi.fn();
store.on('set', SET);
store.state.str = 'hola';
expect(SET).not.toBeCalled();
store.state.str = 'hola2';
expect(SET).toBeCalledWith('str', 'hola2', 'hola');
});
test('custom change detector, values', () => {
const comparer = vi.fn((a, b) => a !== b);
const store = createObservableMap(
{
str: 'hola',
},
comparer,
);
store.state.str = 'hola';
expect(comparer).toBeCalledWith('hola', 'hola', 'str');
store.state.str = 'hola2';
expect(comparer).toBeCalledWith('hola2', 'hola', 'str');
store.state.str = 'hola3';
expect(comparer).toBeCalledWith('hola3', 'hola2', 'str');
});
test('custom change detector, prevent all mutations', () => {
const store = createObservableMap(
{
str: 'hola',
},
() => false,
);
const SET = vi.fn();
store.on('set', SET);
store.state.str = 'hola';
expect(SET).not.toBeCalled();
store.state.str = 'hola2';
expect(SET).not.toBeCalled();
expect(store.state.str).toEqual('hola');
});
describe('use subscriptions', () => {
test('get is called whenever we get a prop', () => {
const store = createObservableMap({ str: 'hola' });
const get = vi.fn();
store.use({ get });
store.state.str;
expect(get).toHaveBeenCalledTimes(1);
expect(get).toHaveBeenCalledWith('str');
});
test('get is unregistered', () => {
const store = createObservableMap({ str: 'hola' });
const get = vi.fn();
const unregister = store.use({ get });
store.state.str;
expect(get).toHaveBeenCalledTimes(1);
get.mockClear();
unregister();
store.state.str;
expect(get).not.toHaveBeenCalled();
});
test('set is called whenever we set a prop', () => {
const store = createObservableMap({ str: 'hola' });
const set = vi.fn();
store.use({ set });
store.state.str = 'adios';
expect(set).toHaveBeenCalledTimes(1);
expect(set).toHaveBeenCalledWith('str', 'adios', 'hola');
});
test('set is unregistered', () => {
const store = createObservableMap({ str: 'hola' });
const set = vi.fn();
const unregister = store.use({ set });
store.state.str = 'adios';
expect(set).toHaveBeenCalledTimes(1);
set.mockClear();
unregister();
store.state.str = 'hello';
expect(set).not.toHaveBeenCalled();
});
test('reset is called when we reset the store', () => {
const store = createObservableMap({ str: 'hola' });
const reset = vi.fn();
store.use({ reset });
store.reset();
expect(reset).toHaveBeenCalledTimes(1);
});
test('reset is unregistered', () => {
const store = createObservableMap({ str: 'hola' });
const reset = vi.fn();
const unregister = store.use({ reset });
store.reset();
expect(reset).toHaveBeenCalledTimes(1);
reset.mockClear();
unregister();
store.reset();
expect(reset).not.toHaveBeenCalled();
});
test('dispose is called when we dispose the store', () => {
const store = createObservableMap({ str: 'hola' });
const dispose = vi.fn();
store.use({ dispose });
store.dispose();
expect(dispose).toHaveBeenCalledTimes(1);
});
test('dispose is unregistered', () => {
const store = createObservableMap({ str: 'hola' });
const dispose = vi.fn();
const unregister = store.use({ dispose });
store.dispose();
expect(dispose).toHaveBeenCalledTimes(1);
dispose.mockClear();
unregister();
store.dispose();
expect(dispose).not.toHaveBeenCalled();
});
test('subscription with several properties subscribes to all of them', () => {
const store = createObservableMap({ str: 'hola' });
const subscription = {
dispose: vi.fn(),
get: vi.fn(),
reset: vi.fn(),
set: vi.fn(),
};
store.use(subscription);
store.state.str;
expect(subscription.get).toHaveBeenCalledTimes(1);
store.state.str = 'adios';
expect(subscription.set).toHaveBeenCalledTimes(1);
store.reset();
expect(subscription.reset).toHaveBeenCalledTimes(1);
store.dispose();
expect(subscription.dispose).toHaveBeenCalledTimes(1);
});
test('subscription with several properties can be unregistered', () => {
const store = createObservableMap({ str: 'hola' });
const subscription = {
dispose: vi.fn(),
get: vi.fn(),
reset: vi.fn(),
set: vi.fn(),
};
const unregister = store.use(subscription);
store.state.str;
expect(subscription.get).toHaveBeenCalledTimes(1);
store.state.str = 'adios';
expect(subscription.set).toHaveBeenCalledTimes(1);
store.reset();
expect(subscription.reset).toHaveBeenCalledTimes(1);
store.dispose();
expect(subscription.dispose).toHaveBeenCalledTimes(1);
vi.clearAllMocks();
unregister();
store.state.str;
expect(subscription.get).not.toHaveBeenCalled();
store.state.str = 'adios';
expect(subscription.set).not.toHaveBeenCalled();
store.reset();
expect(subscription.reset).not.toHaveBeenCalled();
store.dispose();
expect(subscription.dispose).not.toHaveBeenCalled();
});
test('use can be passed several subscriptions', () => {
const store = createObservableMap({ str: 'hola' });
const subscription = {
dispose: vi.fn(),
get: vi.fn(),
reset: vi.fn(),
set: vi.fn(),
};
const subscription2 = {
dispose: vi.fn(),
get: vi.fn(),
reset: vi.fn(),
set: vi.fn(),
};
store.use(subscription, subscription2);
store.state.str;
expect(subscription.get).toHaveBeenCalledTimes(1);
expect(subscription2.get).toHaveBeenCalledTimes(1);
store.state.str = 'adios';
expect(subscription.set).toHaveBeenCalledTimes(1);
expect(subscription2.set).toHaveBeenCalledTimes(1);
store.reset();
expect(subscription.reset).toHaveBeenCalledTimes(1);
expect(subscription2.reset).toHaveBeenCalledTimes(1);
store.dispose();
expect(subscription.dispose).toHaveBeenCalledTimes(1);
expect(subscription2.dispose).toHaveBeenCalledTimes(1);
});
test('use can be passed several subscriptions and unregisters them all', () => {
const store = createObservableMap({ str: 'hola' });
const subscription = {
dispose: vi.fn(),
get: vi.fn(),
reset: vi.fn(),
set: vi.fn(),
};
const subscription2 = {
dispose: vi.fn(),
get: vi.fn(),
reset: vi.fn(),
set: vi.fn(),
};
const unregister = store.use(subscription, subscription2);
store.state.str;
expect(subscription.get).toHaveBeenCalledTimes(1);
expect(subscription2.get).toHaveBeenCalledTimes(1);
store.state.str = 'adios';
expect(subscription.set).toHaveBeenCalledTimes(1);
expect(subscription2.set).toHaveBeenCalledTimes(1);
store.reset();
expect(subscription.reset).toHaveBeenCalledTimes(1);
expect(subscription2.reset).toHaveBeenCalledTimes(1);
store.dispose();
expect(subscription.dispose).toHaveBeenCalledTimes(1);
expect(subscription2.dispose).toHaveBeenCalledTimes(1);
vi.clearAllMocks();
unregister();
store.state.str;
expect(subscription.get).not.toHaveBeenCalled();
expect(subscription2.get).not.toHaveBeenCalled();
store.state.str = 'adios';
expect(subscription.set).not.toHaveBeenCalled();
expect(subscription2.set).not.toHaveBeenCalled();
store.reset();
expect(subscription.reset).not.toHaveBeenCalled();
expect(subscription2.reset).not.toHaveBeenCalled();
store.dispose();
expect(subscription.dispose).not.toHaveBeenCalled();
expect(subscription2.dispose).not.toHaveBeenCalled();
});
});
describe('removeListener', () => {
test('removes a listener from the set event', () => {
const store = createObservableMap({ str: 'hola' });
const listener = vi.fn();
store.onChange('str', listener);
store.state.str = 'adios';
expect(listener).toHaveBeenCalledWith('adios');
store.removeListener('str', listener);
store.state.str = 'hello';
expect(listener).toHaveBeenCalledTimes(1);
});
test('removes a listener from the reset event', () => {
const store = createObservableMap({ str: 'hola' });
const listener = vi.fn();
store.onChange('str', listener);
store.reset();
expect(listener).toHaveBeenCalledWith('hola');
store.removeListener('str', listener);
store.reset();
expect(listener).toHaveBeenCalledTimes(1);
});
test('ignores unknown listeners', () => {
const store = createObservableMap({ str: 'hola' });
const listener = vi.fn();
store.removeListener('str', listener);
// should not throw and should not start tracking the listener
store.set('str', 'hola2');
expect(listener).not.toHaveBeenCalled();
});
});
test('forceUpdate', () => {
const store = createObservableMap({
str: 'hola',
});
const SET = vi.fn();
store.on('set', SET);
store.forceUpdate('str');
store.forceUpdate('str');
expect(SET).toHaveBeenCalledTimes(2);
expect(SET).toBeCalledWith('str', 'hola', 'hola');
});
================================================
FILE: src/observable-map.ts
================================================
import { OnHandler, OnChangeHandler, Subscription, ObservableMap, Handlers } from './types';
type Invocable<T> = T | (() => T);
const unwrap = <T>(val: Invocable<T>): T => (typeof val === 'function' ? (val as () => T)() : val);
export const createObservableMap = <T extends { [key: string]: any }>(
defaultState?: Invocable<T>,
shouldUpdate: (newV: any, oldValue, prop: keyof T) => boolean = (a, b) => a !== b,
): ObservableMap<T> => {
const resolveDefaultState = (): T => (unwrap(defaultState) ?? {}) as T;
const initialState = resolveDefaultState();
let states = new Map<string, any>(Object.entries(initialState));
const proxyAvailable = typeof Proxy !== 'undefined';
const plainState: T | null = proxyAvailable ? null : ({} as T);
const handlers: Handlers<T> = {
dispose: [],
get: [],
set: [],
reset: [],
};
// Track onChange listeners to enable removeListener functionality
const changeListeners = new Map<Function, { setHandler: Function; resetHandler: Function; propName: keyof T }>();
const reset = (): void => {
// When resetting the state, the default state may be a function - unwrap it to invoke it.
// otherwise, the state won't be properly reset
states = new Map<string, any>(Object.entries(resolveDefaultState()));
if (!proxyAvailable) {
syncPlainStateKeys();
}
handlers.reset.forEach((cb) => cb());
};
const dispose = (): void => {
// Call first dispose as resetting the state would
// cause less updates ;)
handlers.dispose.forEach((cb) => cb());
reset();
};
const get = <P extends keyof T>(propName: P & string): T[P] => {
handlers.get.forEach((cb) => cb(propName));
return states.get(propName);
};
const set = <P extends keyof T>(propName: P & string, value: T[P]) => {
const oldValue = states.get(propName);
if (shouldUpdate(value, oldValue, propName)) {
states.set(propName, value);
if (!proxyAvailable) {
ensurePlainProperty(propName as string);
}
handlers.set.forEach((cb) => cb(propName, value, oldValue));
}
};
const state = (
proxyAvailable
? new Proxy(initialState, {
get(_, propName) {
return get(propName as any);
},
ownKeys(_) {
return Array.from(states.keys());
},
getOwnPropertyDescriptor() {
return {
enumerable: true,
configurable: true,
};
},
has(_, propName) {
return states.has(propName as any);
},
set(_, propName, value) {
set(propName as any, value);
return true;
},
})
: (() => {
syncPlainStateKeys();
return plainState!;
})()
) as T;
const on: OnHandler<T> = (eventName, callback) => {
handlers[eventName].push(callback);
return () => {
removeFromArray(handlers[eventName], callback);
};
};
const onChange: OnChangeHandler<T> = (propName, cb) => {
const setHandler = (key, newValue) => {
if (key === propName) {
cb(newValue);
}
};
const resetHandler = () => {
const snapshot = resolveDefaultState();
cb(snapshot[propName]);
};
// Register the handlers
const unSet = on('set', setHandler);
const unReset = on('reset', resetHandler);
// Track the relationship between the user callback and internal handlers
changeListeners.set(cb, { setHandler, resetHandler, propName });
return () => {
unSet();
unReset();
changeListeners.delete(cb);
};
};
const use = (...subscriptions: Subscription<T>[]): (() => void) => {
const unsubs = subscriptions.reduce((unsubs, subscription) => {
if (subscription.set) {
unsubs.push(on('set', subscription.set));
}
if (subscription.get) {
unsubs.push(on('get', subscription.get));
}
if (subscription.reset) {
unsubs.push(on('reset', subscription.reset));
}
if (subscription.dispose) {
unsubs.push(on('dispose', subscription.dispose));
}
return unsubs;
}, []);
return () => unsubs.forEach((unsub) => unsub());
};
const forceUpdate = (key: string) => {
const oldValue = states.get(key);
handlers.set.forEach((cb) => cb(key, oldValue, oldValue));
};
const removeListener = (propName: keyof T, listener: (value: any) => void) => {
const listenerInfo = changeListeners.get(listener);
if (listenerInfo && listenerInfo.propName === propName) {
// Remove the specific handlers that were created for this listener
removeFromArray(handlers.set, listenerInfo.setHandler);
removeFromArray(handlers.reset, listenerInfo.resetHandler);
changeListeners.delete(listener);
}
};
function ensurePlainProperty(key: string): void {
if (proxyAvailable || !plainState) {
return;
}
if (Object.prototype.hasOwnProperty.call(plainState, key)) {
return;
}
Object.defineProperty(plainState, key, {
configurable: true,
enumerable: true,
get() {
return get(key as keyof T & string);
},
set(value: any) {
set(key as keyof T & string, value);
},
});
}
function syncPlainStateKeys(): void {
if (proxyAvailable || !plainState) {
return;
}
const knownKeys = new Set(states.keys());
for (const key of Object.keys(plainState as object)) {
if (!knownKeys.has(key)) {
delete (plainState as Record<string, unknown>)[key];
}
}
for (const key of knownKeys) {
ensurePlainProperty(key);
}
}
return {
state,
get,
set,
on,
onChange,
use,
dispose,
reset,
forceUpdate,
removeListener,
};
};
const removeFromArray = (array: any[], item: any) => {
const index = array.indexOf(item);
if (index >= 0) {
array[index] = array[array.length - 1];
array.length--;
}
};
================================================
FILE: src/store.test.ts
================================================
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { Mock } from 'vitest';
vi.mock('./observable-map', () => {
return {
createObservableMap: vi.fn(),
};
});
vi.mock('./subscriptions/stencil', () => {
return {
stencilSubscription: vi.fn(),
};
});
afterEach(() => {
vi.clearAllMocks();
});
describe('createStore', () => {
it('creates an observable map and wires the stencil subscription', async () => {
const map = { use: vi.fn(), state: {} } as const;
const subscription = { get: vi.fn() };
const { createObservableMap } = await import('./observable-map');
const { stencilSubscription } = await import('./subscriptions/stencil');
(createObservableMap as unknown as Mock).mockReturnValue(map as any);
(stencilSubscription as unknown as Mock).mockReturnValue(subscription as any);
const { createStore } = await import('./store');
const defaultState = { value: 1 };
const shouldUpdate = vi.fn();
const result = createStore(defaultState, shouldUpdate);
expect(createObservableMap).toHaveBeenCalledWith(defaultState, shouldUpdate);
expect(map.use).toHaveBeenCalledWith(subscription);
expect(result).toBe(map);
});
});
================================================
FILE: src/store.ts
================================================
import { stencilSubscription } from './subscriptions/stencil';
import { createObservableMap } from './observable-map';
import { ObservableMap } from './types';
export const createStore = <T extends { [key: string]: any }>(
defaultState?: T | (() => T),
shouldUpdate?: (newV: any, oldValue, prop: keyof T) => boolean,
): ObservableMap<T> => {
const map = createObservableMap(defaultState, shouldUpdate);
map.use(stencilSubscription());
return map;
};
================================================
FILE: src/subscriptions/stencil.test.ts
================================================
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const coreMock = vi.hoisted(() => ({
exports: {} as {
forceUpdate?: ReturnType<typeof vi.fn>;
getRenderingRef?: ReturnType<typeof vi.fn>;
},
}));
vi.mock('@stencil/core', () => coreMock.exports);
describe('stencilSubscription', () => {
beforeEach(() => {
coreMock.exports.forceUpdate = undefined;
coreMock.exports.getRenderingRef = undefined;
});
afterEach(() => {
vi.clearAllMocks();
vi.resetModules();
vi.useRealTimers();
});
it('returns an empty subscription when stencil internals are unavailable', async () => {
coreMock.exports.forceUpdate = vi.fn();
coreMock.exports.getRenderingRef = undefined;
const { stencilSubscription } = await import('./stencil');
expect(stencilSubscription()).toEqual({});
});
it('tracks stencil elements and triggers updates', async () => {
vi.useFakeTimers();
const connectedElm = { isConnected: true, id: 'connected' };
const disconnectedElm = { isConnected: false, id: 'disconnected' };
const legacyElm = { id: 'legacy' } as { id: string; isConnected?: boolean };
const forceUpdate = vi.fn((elm: typeof connectedElm) => elm.isConnected !== false);
const getRenderingRef = vi
.fn()
.mockReturnValueOnce(connectedElm)
.mockReturnValueOnce(disconnectedElm)
.mockReturnValueOnce(legacyElm)
.mockReturnValueOnce(undefined);
coreMock.exports.forceUpdate = forceUpdate as any;
coreMock.exports.getRenderingRef = getRenderingRef as any;
const { stencilSubscription } = await import('./stencil');
const subscription = stencilSubscription();
subscription.set?.('missing');
expect(forceUpdate).not.toHaveBeenCalled();
subscription.get?.('prop');
subscription.get?.('prop');
subscription.get?.('prop');
subscription.get?.('prop');
expect(getRenderingRef).toHaveBeenCalledTimes(4);
subscription.set?.('prop');
expect(forceUpdate).toHaveBeenCalledTimes(3);
expect(forceUpdate.mock.calls[0]?.[0]).toBe(connectedElm);
expect(forceUpdate.mock.calls[1]?.[0]).toBe(disconnectedElm);
expect(forceUpdate.mock.calls[2]?.[0]).toBe(legacyElm);
vi.runAllTimers();
forceUpdate.mockClear();
subscription.reset?.();
expect(forceUpdate).toHaveBeenCalledTimes(2);
expect(forceUpdate.mock.calls[0]?.[0]).toBe(connectedElm);
expect(forceUpdate.mock.calls[1]?.[0]).toBe(legacyElm);
vi.runAllTimers();
forceUpdate.mockClear();
subscription.dispose?.();
subscription.reset?.();
expect(forceUpdate).not.toHaveBeenCalled();
});
it('prevents duplicate subscriptions for the same element', async () => {
const elm = { isConnected: true, id: 'unique' };
const forceUpdate = vi.fn();
const getRenderingRef = vi.fn().mockReturnValue(elm);
coreMock.exports.forceUpdate = forceUpdate as any;
coreMock.exports.getRenderingRef = getRenderingRef as any;
const { stencilSubscription } = await import('./stencil');
const subscription = stencilSubscription();
subscription.get?.('prop');
subscription.get?.('prop');
expect(getRenderingRef).toHaveBeenCalledTimes(2);
subscription.set?.('prop');
expect(forceUpdate).toHaveBeenCalledTimes(1);
});
it('handles garbage collected elements', async () => {
const originalWeakRef = global.WeakRef;
const gcedElm = { id: 'gced' };
const keptElm = { id: 'kept', isConnected: true };
class MockWeakRef {
target: any;
constructor(target: any) {
this.target = target;
}
deref() {
if (this.target === gcedElm) return undefined;
return this.target;
}
}
(global as any).WeakRef = MockWeakRef;
const forceUpdate = vi.fn(() => true);
const getRenderingRef = vi.fn().mockReturnValueOnce(gcedElm).mockReturnValueOnce(keptElm);
coreMock.exports.forceUpdate = forceUpdate as any;
coreMock.exports.getRenderingRef = getRenderingRef as any;
try {
const { stencilSubscription } = await import('./stencil');
const subscription = stencilSubscription();
subscription.get?.('prop');
subscription.get?.('prop');
subscription.set?.('prop');
expect(forceUpdate).toHaveBeenCalledTimes(1);
expect(forceUpdate).toHaveBeenCalledWith(keptElm);
forceUpdate.mockClear();
subscription.reset?.();
expect(forceUpdate).toHaveBeenCalledTimes(1);
expect(forceUpdate).toHaveBeenCalledWith(keptElm);
} finally {
global.WeakRef = originalWeakRef;
}
});
});
================================================
FILE: src/subscriptions/stencil.ts
================================================
import * as StencilCore from '@stencil/core';
import { Subscription } from '../types';
import { appendToMap, debounce } from '../utils';
/**
* Check if a possible element isConnected.
* The property might not be there, so we check for it.
*
* We want it to return true if isConnected is not a property,
* otherwise we would remove these elements and would not update.
*
* Better leak in Edge than to be useless.
*/
const isConnected = (maybeElement: any) => !('isConnected' in maybeElement) || maybeElement.isConnected;
const cleanupElements = debounce((map: Map<string, WeakRef<any>[]>) => {
for (let key of map.keys()) {
const refs = map.get(key).filter((ref) => {
const elm = ref.deref();
return elm && isConnected(elm);
});
map.set(key, refs);
}
}, 2_000);
const core = StencilCore as unknown as {
forceUpdate?: (elm: any) => boolean;
getRenderingRef?: () => any;
};
const forceUpdate = core.forceUpdate;
const getRenderingRef = core.getRenderingRef;
export const stencilSubscription = <T>(): Subscription<T> => {
if (typeof getRenderingRef !== 'function' || typeof forceUpdate !== 'function') {
// If we are not in a stencil project, we do nothing.
// This function is not really exported by @stencil/core.
return {};
}
const ensureForceUpdate = forceUpdate;
const ensureGetRenderingRef = getRenderingRef;
const elmsToUpdate = new Map<string, WeakRef<any>[]>();
return {
dispose: () => elmsToUpdate.clear(),
get: (propName) => {
const elm = ensureGetRenderingRef();
if (elm) {
appendToMap(elmsToUpdate, propName as string, elm);
}
},
set: (propName) => {
const refs = elmsToUpdate.get(propName as string);
if (refs) {
const nextRefs = refs.filter((ref) => {
const elm = ref.deref();
if (!elm) return false;
return ensureForceUpdate(elm);
});
elmsToUpdate.set(propName as string, nextRefs);
}
cleanupElements(elmsToUpdate);
},
reset: () => {
elmsToUpdate.forEach((refs) => {
refs.forEach((ref) => {
const elm = ref.deref();
if (elm) ensureForceUpdate(elm);
});
});
cleanupElements(elmsToUpdate);
},
};
};
================================================
FILE: src/types.ts
================================================
export interface Handlers<T> {
dispose: DisposeEventHandler[];
get: GetEventHandler<T>[];
reset: ResetEventHandler[];
set: SetEventHandler<T>[];
}
export type SetEventHandler<StoreType> = (key: keyof StoreType, newValue: any, oldValue: any) => void;
export type GetEventHandler<StoreType> = (key: keyof StoreType) => void;
export type ResetEventHandler = () => void;
export type DisposeEventHandler = () => void;
export interface OnHandler<StoreType> {
(eventName: 'set', callback: SetEventHandler<StoreType>): () => void;
(eventName: 'get', callback: GetEventHandler<StoreType>): () => void;
(eventName: 'dispose', callback: DisposeEventHandler): () => void;
(eventName: 'reset', callback: ResetEventHandler): () => void;
}
export interface OnChangeHandler<StoreType> {
<Key extends keyof StoreType>(propName: Key, cb: (newValue: StoreType[Key]) => void): () => void;
}
export interface Subscription<StoreType> {
dispose?(): void;
get?<KeyFromStoreType extends keyof StoreType>(key: KeyFromStoreType): void;
set?<KeyFromStoreType extends keyof StoreType>(
key: KeyFromStoreType,
newValue: StoreType[KeyFromStoreType],
oldValue: StoreType[KeyFromStoreType],
): void;
reset?(): void;
}
export interface Getter<T> {
<P extends keyof T>(propName: P & string): T[P];
}
export interface Setter<T> {
<P extends keyof T>(propName: P & string, value: T[P]): void;
}
export interface ObservableMap<T> {
/**
* Proxied object that will detect dependencies and call
* the subscriptions and computed properties.
*
* If available, it will detect from which Stencil Component
* it was called and rerender it when the property changes.
*
* Note: Proxy objects are not supported by IE11 (not even with a polyfill)
* so you need to use the store.get and store.set methods of the API if you wish to support IE11.
*
* @returns The proxied object
*/
state: T;
/**
* Only useful if you need to support IE11.
*
* @param propName - The property name to get
* @returns The value of the property
*
* @example
* const { state, get } = createStore({ hola: 'hello', adios: 'goodbye' });
* console.log(state.hola); // If you don't need to support IE11, use this way.
* console.log(get('hola')); // If you need to support IE11, use this other way.
*/
get: Getter<T>;
/**
* Only useful if you need to support IE11.
*
* @param propName - The property name to set
* @param value - The value to set
* @returns void
*
* @example
* const { state, get } = createStore({ hola: 'hello', adios: 'goodbye' });
* state.hola = 'ola'; // If you don't need to support IE11, use this way.
* set('hola', 'ola')); // If you need to support IE11, use this other way.
*/
set: Setter<T>;
/**
* Register a event listener, you can listen to `set`, `get` and `reset` events.
*
* @param eventName - The event name to listen for
* @param callback - The callback to call when the event occurs
* @returns A function to unsubscribe from the listener
*
* @example
* store.on('set', (prop, value) => {
* console.log(`Prop ${prop} changed to: ${value}`);
* });
*/
on: OnHandler<T>;
/**
* Easily listen for value changes of the specified key.
*
* @param propName - The property name to listen for
* @param cb - The callback to call when the property changes
* @returns A function to unsubscribe from the listener
*/
onChange: OnChangeHandler<T>;
/**
* Resets the state to its original state and
* signals a dispose event to all the plugins.
*
* This method is intended for plugins to reset
* all their internal state between tests.
*
* @returns void
*/
dispose(): void;
/**
* Resets the state to its original state.
*
* @returns void
*/
reset(): void;
/**
* Registers a subscription that will be called whenever the user gets, sets, or
* resets a value.
*
* @param plugins - The plugins to use
* @returns A function to unsubscribe from the plugins
*/
use(...plugins: Subscription<T>[]): () => void;
/**
* Force a rerender of the specified key, just like the value changed.
*
* @param key - The property name to force an update for
* @returns void
*/
forceUpdate(key: keyof T): void;
/**
* Remove a listener
*
* @param propName - The property name to remove the listener from
* @param listener - The listener to remove
* @returns void
*/
removeListener(propName: keyof T, listener: (value: any) => void): void;
}
================================================
FILE: src/utils.test.ts
================================================
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { appendToMap, debounce } from './utils';
describe('appendToMap', () => {
let testMap: Map<string, WeakRef<Object>[]>;
beforeEach(() => {
testMap = new Map();
});
it('should add value to empty map', () => {
const obj = { id: 1 };
appendToMap(testMap, 'key1', obj);
const refs = testMap.get('key1');
expect(refs).toHaveLength(1);
expect(refs![0].deref()).toBe(obj);
});
it('should append value to existing array', () => {
const obj1 = { id: 1 };
const obj2 = { id: 3 };
appendToMap(testMap, 'key1', obj1);
appendToMap(testMap, 'key1', obj2);
const refs = testMap.get('key1');
expect(refs).toHaveLength(2);
expect(refs![0].deref()).toBe(obj1);
expect(refs![1].deref()).toBe(obj2);
});
it('should not append duplicate value', () => {
const obj1 = { id: 1 };
const obj2 = { id: 2 };
appendToMap(testMap, 'key1', obj1);
appendToMap(testMap, 'key1', obj2);
appendToMap(testMap, 'key1', obj1); // Duplicate
const refs = testMap.get('key1');
expect(refs).toHaveLength(2);
expect(refs![0].deref()).toBe(obj1);
expect(refs![1].deref()).toBe(obj2);
});
});
describe('debounce', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should debounce function calls', () => {
const mockFn = vi.fn();
const debouncedFn = debounce(mockFn, 1000);
// Call the debounced function multiple times
debouncedFn(1);
debouncedFn(2);
debouncedFn(3);
// Function should not have been called yet
expect(mockFn).not.toHaveBeenCalled();
// Fast forward time
vi.runAllTimers();
// Function should have been called once with the last arguments
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith(3);
});
it('should cancel previous timeout on new calls', () => {
const mockFn = vi.fn();
const debouncedFn = debounce(mockFn, 1000);
debouncedFn(1);
// Advance timer halfway
vi.advanceTimersByTime(500);
debouncedFn(2);
// Advance to just before the second call would trigger
vi.advanceTimersByTime(999);
expect(mockFn).not.toHaveBeenCalled();
// Advance the remaining time
vi.advanceTimersByTime(1);
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledWith(2);
});
});
================================================
FILE: src/utils.ts
================================================
export const appendToMap = <K, V extends Object>(map: Map<K, WeakRef<V>[]>, propName: K, value: V) => {
let refs = map.get(propName);
if (!refs) {
refs = [];
map.set(propName, refs);
}
if (!refs.some((ref) => ref.deref() === value)) {
refs.push(new WeakRef(value));
}
};
export const debounce = <T extends (...args: any[]) => any>(fn: T, ms: number): ((...args: Parameters<T>) => void) => {
let timeoutId: any;
return (...args: Parameters<T>) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
timeoutId = 0;
fn(...args);
}, ms);
};
};
================================================
FILE: test-app/.editorconfig
================================================
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
================================================
FILE: test-app/.gitignore
================================================
dist/
www/
loader/
*~
*.sw[mnpcod]
*.log
*.lock
*.tmp
*.tmp.*
log.txt
*.sublime-project
*.sublime-workspace
.stencil/
.idea/
.vscode/
.sass-cache/
.versions/
node_modules/
$RECYCLE.BIN/
.DS_Store
Thumbs.db
UserInterfaceState.xcuserstate
.env
================================================
FILE: test-app/.prettierrc.json
================================================
{
"parser": "typescript",
"printWidth": 100,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"jsxBracketSameLine": false,
"jsxSingleQuote": false
}
================================================
FILE: test-app/LICENSE
================================================
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: test-app/package.json
================================================
{
"name": "stencil-store-tests",
"version": "0.0.1",
"description": "Stencil Component Starter",
"main": "./dist/index.cjs.js",
"module": "./dist/index.js",
"es2015": "dist/esm/index.mjs",
"es2017": "dist/esm/index.mjs",
"types": "dist/types/components.d.ts",
"collection": "dist/collection/collection-manifest.json",
"collection:main": "dist/collection/index.js",
"unpkg": "dist/stencil-store-tests/stencil-store-tests.js",
"files": [
"dist/",
"loader/"
],
"scripts": {
"build": "stencil build",
"start": "stencil build --dev --watch --serve",
"lint": "npm run lint.prettier",
"test": "stencil test --spec --e2e",
"test.spec": "stencil test --spec",
"test.ci": "npm run test",
"test.watch": "stencil test --spec --e2e --watchAll",
"generate": "stencil generate"
},
"devDependencies": {
"@stencil/core": "^4.0.0",
"@stencil/store": "latest",
"@types/node": "^20.11.7",
"puppeteer": "^22.15.0"
},
"license": "MIT"
}
================================================
FILE: test-app/readme.md
================================================

# Stencil Component Starter
> This is a starter project for building a standalone Web Component using Stencil.
Stencil is a compiler for building fast web apps using Web Components.
Stencil combines the best concepts of the most popular frontend frameworks into a compile-time rather than run-time tool. Stencil takes TypeScript, JSX, a tiny virtual DOM layer, efficient one-way data binding, an asynchronous rendering pipeline (similar to React Fiber), and lazy-loading out of the box, and generates 100% standards-based Web Components that run in any browser supporting the Custom Elements v1 spec.
Stencil components are just Web Components, so they work in any major framework or with no framework at all.
## Getting Started
To start building a new web component using Stencil, clone this repo to a new directory:
```bash
git clone https://github.com/stenciljs/component-starter.git my-component
cd my-component
git remote rm origin
```
and run:
```bash
npm install
npm start
```
To build the component for production, run:
```bash
npm run build
```
To run the unit tests for the components, run:
```bash
npm test
```
Need help? Check out our docs [here](https://stenciljs.com/docs/my-first-component).
## Naming Components
When creating new component tags, we recommend _not_ using `stencil` in the component name (ex: `<stencil-datepicker>`). This is because the generated component has little to nothing to do with Stencil; it's just a web component!
Instead, use a prefix that fits your company or any name for a group of related components. For example, all of the Ionic generated web components use the prefix `ion`.
## Using this component
### Script tag
- [Publish to NPM](https://docs.npmjs.com/getting-started/publishing-npm-packages)
- Put a script tag similar to this `<script src='https://unpkg.com/my-component@0.0.1/dist/mycomponent.js'></script>` in the head of your index.html
- Then you can use the element anywhere in your template, JSX, html etc
### Node Modules
- Run `npm install my-component --save`
- Put a script tag similar to this `<script src='node_modules/my-component/dist/mycomponent.js'></script>` in the head of your index.html
- Then you can use the element anywhere in your template, JSX, html etc
### In a stencil-starter app
- Run `npm install my-component --save`
- Add an import to the npm packages `import my-component;`
- Then you can use the element anywhere in your template, JSX, html etc
================================================
FILE: test-app/src/components/change-store/change-store.tsx
================================================
import { Component, Prop, Host, h } from '@stencil/core';
import { state } from '../../utils/greeting-store';
@Component({
tag: 'change-store',
shadow: false,
})
export class ChangeStore {
@Prop() storeKey: 'hola' | 'adios';
@Prop() storeValue: string;
changeValue() {
state[this.storeKey] = this.storeValue;
}
render() {
return (
<Host>
<button onClick={() => this.changeValue()}>Change!</button>
</Host>
);
}
}
================================================
FILE: test-app/src/components/display-store/display-store.e2e.ts
================================================
import { newE2EPage, E2EPage } from '@stencil/core/testing';
describe('display-store', () => {
it('re-renders', async () => {
const page = await newE2EPage();
await page.setContent(
'<display-store store-key="hello"></display-store><change-store store-key="hello" store-value="other-value"></change-store>'
);
expect(await displayedValue(page)).toEqual('hola');
expect(await renderCount(page)).toEqual(1);
await changeValue(page);
expect(await displayedValue(page)).toEqual('other-value');
expect(await renderCount(page)).toEqual(2);
});
it('does not rerender if the key changed is a different one', async () => {
const page = await newE2EPage();
await page.setContent(
'<display-store class="hello" store-key="hello"></display-store><display-store class="goodbye" store-key="goodbye"></display-store><change-store store-key="hello" store-value="other-value"></change-store>'
);
expect(await displayedValue(page, '.hello')).toEqual('hola');
expect(await renderCount(page, '.hello')).toEqual(1);
expect(await displayedValue(page, '.goodbye')).toEqual('adiós');
expect(await renderCount(page, '.goodbye')).toEqual(1);
await changeValue(page);
expect(await displayedValue(page, '.hello')).toEqual('other-value');
expect(await renderCount(page, '.hello')).toEqual(2);
expect(await displayedValue(page, '.goodbye')).toEqual('adiós');
expect(await renderCount(page, '.goodbye')).toEqual(1);
});
});
const displayedValue = async (page: E2EPage, parentSelector?: string): Promise<string> => {
const parentElement = parentSelector === undefined ? page : await page.find(parentSelector);
const element = await parentElement.find('.value');
return element.textContent;
};
const renderCount = async (page: E2EPage, parentSelector?: string): Promise<number> => {
const parentElement = parentSelector === undefined ? page : await page.find(parentSelector);
const element = await parentElement.find('.counter');
return parseInt(element.textContent, 10);
};
const changeValue = async (page: E2EPage, newValue?: string): Promise<void> => {
if (newValue !== undefined) {
const element = await page.find('change-store');
element.setProperty('store-value', newValue);
}
const button = await page.find('button');
await button.click();
await page.waitForChanges();
};
================================================
FILE: test-app/src/components/display-store/display-store.tsx
================================================
import { Component, Prop, h, Host } from '@stencil/core';
import { state } from '../../utils/greeting-store';
@Component({
tag: 'display-store',
shadow: false,
})
export class DisplayStore {
@Prop() storeKey: 'hello' | 'goodbye';
i = 0;
render() {
this.i++;
return (
<Host>
<span class="counter">{this.i}</span>
<span class="value">{state[this.storeKey]}</span>
</Host>
);
}
}
================================================
FILE: test-app/src/components/simple-store/display-store.spec.ts
================================================
import { newSpecPage } from '@stencil/core/testing';
import { SimpleStore } from './display-store';
import { dispose, reset } from '../../utils/greeting-store';
describe('some-store', () => {
beforeEach(() => dispose());
it('updates', async () => {
reset();
const { root, waitForChanges } = await newSpecPage({
components: [SimpleStore],
html: `<simple-store></simple-store>`,
});
expect(root).toEqualHtml(`
<simple-store>
hola
<span>0</span>
<span>0</span>
</simple-store>
`);
await root.next();
await waitForChanges();
expect(root).toEqualHtml(`
<simple-store>
hola
<span>1</span>
<span>1</span>
</simple-store>
`);
await root.next();
await waitForChanges();
expect(root).toEqualHtml(`
<simple-store>
hola
<span>2</span>
<span>4</span>
</simple-store>
`);
reset();
await waitForChanges();
expect(root).toEqualHtml(`
<simple-store>
hola
<span>0</span>
<span>0</span>
</simple-store>
`);
});
it('resetting in a second test does not crash', async () => {
reset();
const { root, waitForChanges } = await newSpecPage({
components: [SimpleStore],
html: `<simple-store></simple-store>`,
});
expect(root).toEqualHtml(`
<simple-store>
hola
<span>0</span>
<span>0</span>
</simple-store>
`);
await root.next();
await waitForChanges();
expect(root).toEqualHtml(`
<simple-store>
hola
<span>1</span>
<span>1</span>
</simple-store>
`);
await root.next();
await waitForChanges();
expect(root).toEqualHtml(`
<simple-store>
hola
<span>2</span>
<span>4</span>
</simple-store>
`);
reset();
await waitForChanges();
expect(root).toEqualHtml(`
<simple-store>
hola
<span>0</span>
<span>0</span>
</simple-store>
`);
});
});
================================================
FILE: test-app/src/components/simple-store/display-store.tsx
================================================
import { Component, h, Host, Method } from '@stencil/core';
import { state } from '../../utils/greeting-store';
@Component({
tag: 'simple-store',
shadow: false,
})
export class SimpleStore {
@Method()
async next() {
state.clicks++;
}
render() {
return (
<Host>
{state.hello}
<span>{state.clicks}</span>
<span>{state.squaredClicks}</span>
</Host>
);
}
}
================================================
FILE: test-app/src/components.d.ts
================================================
/* eslint-disable */
/* tslint:disable */
/**
* This is an autogenerated file created by the Stencil compiler.
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
export namespace Components {
interface ChangeStore {
"storeKey": 'hola' | 'adios';
"storeValue": string;
}
interface DisplayStore {
"storeKey": 'hello' | 'goodbye';
}
interface SimpleStore {
"next": () => Promise<void>;
}
}
declare global {
interface HTMLChangeStoreElement extends Components.ChangeStore, HTMLStencilElement {
}
var HTMLChangeStoreElement: {
prototype: HTMLChangeStoreElement;
new (): HTMLChangeStoreElement;
};
interface HTMLDisplayStoreElement extends Components.DisplayStore, HTMLStencilElement {
}
var HTMLDisplayStoreElement: {
prototype: HTMLDisplayStoreElement;
new (): HTMLDisplayStoreElement;
};
interface HTMLSimpleStoreElement extends Components.SimpleStore, HTMLStencilElement {
}
var HTMLSimpleStoreElement: {
prototype: HTMLSimpleStoreElement;
new (): HTMLSimpleStoreElement;
};
interface HTMLElementTagNameMap {
"change-store": HTMLChangeStoreElement;
"display-store": HTMLDisplayStoreElement;
"simple-store": HTMLSimpleStoreElement;
}
}
declare namespace LocalJSX {
interface ChangeStore {
"storeKey"?: 'hola' | 'adios';
"storeValue"?: string;
}
interface DisplayStore {
"storeKey"?: 'hello' | 'goodbye';
}
interface SimpleStore {
}
interface IntrinsicElements {
"change-store": ChangeStore;
"display-store": DisplayStore;
"simple-store": SimpleStore;
}
}
export { LocalJSX as JSX };
declare module "@stencil/core" {
export namespace JSX {
interface IntrinsicElements {
"change-store": LocalJSX.ChangeStore & JSXBase.HTMLAttributes<HTMLChangeStoreElement>;
"display-store": LocalJSX.DisplayStore & JSXBase.HTMLAttributes<HTMLDisplayStoreElement>;
"simple-store": LocalJSX.SimpleStore & JSXBase.HTMLAttributes<HTMLSimpleStoreElement>;
}
}
}
================================================
FILE: test-app/src/index.html
================================================
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0">
<title>Stencil Component Starter</title>
<script type="module" src="/build/stencil-store-tests.esm.js"></script>
<script nomodule src="/build/stencil-store-tests.js"></script>
</head>
<body>
<display-store store-key="hello"></display-store>
<change-store store-key="hello" store-value="other-value"></change-store>
</body>
</html>
================================================
FILE: test-app/src/index.ts
================================================
export * from './components';
================================================
FILE: test-app/src/utils/greeting-store.ts
================================================
import { createStore } from '@stencil/store';
const store = createStore({
hello: 'hola',
goodbye: 'adiós',
clicks: 0,
squaredClicks: 0,
});
store.onChange('clicks', (value) => {
state.squaredClicks = value ** 2;
});
export const dispose = store.dispose;
export const state = store.state;
export const reset = store.reset;
================================================
FILE: test-app/stencil.config.ts
================================================
import { Config } from '@stencil/core';
export const config: Config = {
namespace: 'stencil-store-tests',
outputTargets: [
{
type: 'dist',
esmLoaderPath: '../loader'
},
{
type: 'www',
serviceWorker: null // disable service workers
}
]
};
================================================
FILE: test-app/tsconfig.json
================================================
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"allowUnreachableCode": false,
"declaration": false,
"experimentalDecorators": true,
"lib": [
"dom",
"es2017"
],
"moduleResolution": "node",
"module": "esnext",
"target": "es2017",
"noUnusedLocals": true,
"noUnusedParameters": true,
"jsx": "react",
"jsxFactory": "h"
},
"include": [
"src",
"types/jsx.d.ts"
],
"exclude": [
"node_modules"
]
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"allowUnreachableCode": false,
"declaration": true,
"experimentalDecorators": true,
"lib": [
"dom",
"es2022"
],
"outDir": "build",
"declarationDir": "dist",
"moduleResolution": "bundler",
"module": "esnext",
"target": "es2022",
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"jsx": "react",
"jsxFactory": "h",
"useUnknownInCatchVariables": true
},
"files": [
"src/index.ts"
],
"exclude": [
"node_modules",
"src/**/*.test.ts"
]
}
================================================
FILE: vitest.config.ts
================================================
import { defineConfig } from 'vitest/config';
const exclude = ['node_modules', 'dist', 'test-app'];
export default defineConfig({
test: {
exclude,
coverage: {
enabled: true,
exclude: [
...exclude,
'vitest.config.ts',
'rollup.config.js'
],
provider: 'v8',
thresholds: {
branches: 93,
functions: 86,
lines: 80,
statements: 80,
},
},
},
});
gitextract_14714bwu/ ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ ├── ionic-issue-bot.yml │ └── workflows/ │ ├── build.yml │ ├── main.yml │ ├── publish-npm.yml │ ├── release-dev.yml │ ├── release-orchestrator.yml │ └── release-production.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── rollup.config.js ├── src/ │ ├── index.test.ts │ ├── index.ts │ ├── observable-map.test.ts │ ├── observable-map.ts │ ├── store.test.ts │ ├── store.ts │ ├── subscriptions/ │ │ ├── stencil.test.ts │ │ └── stencil.ts │ ├── types.ts │ ├── utils.test.ts │ └── utils.ts ├── test-app/ │ ├── .editorconfig │ ├── .gitignore │ ├── .prettierrc.json │ ├── LICENSE │ ├── package.json │ ├── readme.md │ ├── src/ │ │ ├── components/ │ │ │ ├── change-store/ │ │ │ │ └── change-store.tsx │ │ │ ├── display-store/ │ │ │ │ ├── display-store.e2e.ts │ │ │ │ └── display-store.tsx │ │ │ └── simple-store/ │ │ │ ├── display-store.spec.ts │ │ │ └── display-store.tsx │ │ ├── components.d.ts │ │ ├── index.html │ │ ├── index.ts │ │ └── utils/ │ │ └── greeting-store.ts │ ├── stencil.config.ts │ └── tsconfig.json ├── tsconfig.json └── vitest.config.ts
SYMBOL INDEX (42 symbols across 7 files)
FILE: src/observable-map.ts
type Invocable (line 3) | type Invocable<T> = T | (() => T);
method get (line 64) | get(_, propName) {
method ownKeys (line 67) | ownKeys(_) {
method getOwnPropertyDescriptor (line 70) | getOwnPropertyDescriptor() {
method has (line 76) | has(_, propName) {
method set (line 79) | set(_, propName, value) {
function ensurePlainProperty (line 159) | function ensurePlainProperty(key: string): void {
function syncPlainStateKeys (line 180) | function syncPlainStateKeys(): void {
FILE: src/subscriptions/stencil.test.ts
class MockWeakRef (line 117) | class MockWeakRef {
method constructor (line 119) | constructor(target: any) {
method deref (line 122) | deref() {
FILE: src/types.ts
type Handlers (line 1) | interface Handlers<T> {
type SetEventHandler (line 8) | type SetEventHandler<StoreType> = (key: keyof StoreType, newValue: any, ...
type GetEventHandler (line 9) | type GetEventHandler<StoreType> = (key: keyof StoreType) => void;
type ResetEventHandler (line 10) | type ResetEventHandler = () => void;
type DisposeEventHandler (line 11) | type DisposeEventHandler = () => void;
type OnHandler (line 13) | interface OnHandler<StoreType> {
type OnChangeHandler (line 20) | interface OnChangeHandler<StoreType> {
type Subscription (line 24) | interface Subscription<StoreType> {
type Getter (line 35) | interface Getter<T> {
type Setter (line 39) | interface Setter<T> {
type ObservableMap (line 43) | interface ObservableMap<T> {
FILE: test-app/src/components.d.ts
type ChangeStore (line 9) | interface ChangeStore {
type DisplayStore (line 13) | interface DisplayStore {
type SimpleStore (line 16) | interface SimpleStore {
type HTMLChangeStoreElement (line 21) | interface HTMLChangeStoreElement extends Components.ChangeStore, HTMLSte...
type HTMLDisplayStoreElement (line 27) | interface HTMLDisplayStoreElement extends Components.DisplayStore, HTMLS...
type HTMLSimpleStoreElement (line 33) | interface HTMLSimpleStoreElement extends Components.SimpleStore, HTMLSte...
type HTMLElementTagNameMap (line 39) | interface HTMLElementTagNameMap {
type ChangeStore (line 46) | interface ChangeStore {
type DisplayStore (line 50) | interface DisplayStore {
type SimpleStore (line 53) | interface SimpleStore {
type IntrinsicElements (line 55) | interface IntrinsicElements {
type IntrinsicElements (line 64) | interface IntrinsicElements {
FILE: test-app/src/components/change-store/change-store.tsx
class ChangeStore (line 8) | class ChangeStore {
method changeValue (line 12) | changeValue() {
method render (line 16) | render() {
FILE: test-app/src/components/display-store/display-store.tsx
class DisplayStore (line 8) | class DisplayStore {
method render (line 13) | render() {
FILE: test-app/src/components/simple-store/display-store.tsx
class SimpleStore (line 8) | class SimpleStore {
method next (line 11) | async next() {
method render (line 15) | render() {
Condensed preview — 52 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (104K chars).
[
{
"path": ".github/CODEOWNERS",
"chars": 42,
"preview": "* @stenciljs/technical-steering-committee\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 69,
"preview": "# These are supported funding model platforms\n\ngithub: [johnjenkins]\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 2391,
"preview": "name: 🐛 Bug Report\ndescription: Create a report to help us improve Stencil Store\ntitle: 'bug: '\nbody:\n - type: checkbox"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 823,
"preview": "contact_links:\n - name: 💻 Stencil\n url: https://github.com/stenciljs/core/issues/new/choose\n about: This issue tr"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 1999,
"preview": "name: 💡 Feature Request\ndescription: Suggest an idea for Stencil Store\ntitle: 'feat: '\nbody:\n - type: checkboxes\n at"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 1803,
"preview": "<!-- Please refer to our contributing documentation for any questions on submitting a pull request, or let us know here "
},
{
"path": ".github/dependabot.yml",
"chars": 668,
"preview": "version: 2\nupdates:\n- package-ecosystem: npm\n directory: \"/\"\n schedule:\n interval: weekly\n open-pull-requests-limi"
},
{
"path": ".github/ionic-issue-bot.yml",
"chars": 3086,
"preview": "triage:\n label: triage\n dryRun: false\n\ncloseAndLock:\n labels:\n - label: \"ionitron: support\"\n message: >\n "
},
{
"path": ".github/workflows/build.yml",
"chars": 1115,
"preview": "name: Build Stencil Store\n\non:\n workflow_call:\n # Make this a reusable workflow, no value needed\n # https://docs.gith"
},
{
"path": ".github/workflows/main.yml",
"chars": 2203,
"preview": "name: CI\n\non:\n push:\n branches:\n - 'main'\n pull_request:\n branches:\n - '**'\n\njobs:\n build:\n name: "
},
{
"path": ".github/workflows/publish-npm.yml",
"chars": 6354,
"preview": "name: 'Publish Stencil Store'\n\non:\n workflow_call:\n inputs:\n version:\n description: 'Version or semver b"
},
{
"path": ".github/workflows/release-dev.yml",
"chars": 1088,
"preview": "name: 'Stencil Store Dev Release'\n\non:\n workflow_call:\n\npermissions:\n contents: write\n id-token: write\n\njobs:\n build"
},
{
"path": ".github/workflows/release-orchestrator.yml",
"chars": 829,
"preview": "name: 'Release - Stencil Store'\n\non:\n workflow_dispatch:\n inputs:\n channel:\n description: 'Which release"
},
{
"path": ".github/workflows/release-production.yml",
"chars": 1053,
"preview": "name: 'Stencil Store Production Release'\n\non:\n workflow_call:\n inputs:\n bump:\n description: 'Semver bump"
},
{
"path": ".gitignore",
"chars": 266,
"preview": "dist/\nwww/\nloader/\nbuild/\n\n*~\n*.sw[mnpcod]\n*.log\n*.lock\n*.tmp\n*.tmp.*\nlog.txt\n*.sublime-project\n*.sublime-workspace\n*.tg"
},
{
"path": ".nvmrc",
"chars": 8,
"preview": "v22.14.0"
},
{
"path": ".prettierrc.json",
"chars": 114,
"preview": "{\n \"parser\": \"typescript\",\n \"printWidth\": 100,\n \"semi\": true,\n \"singleQuote\": true,\n \"trailingComma\": \"es5\"\n}"
},
{
"path": "CONTRIBUTING.md",
"chars": 7662,
"preview": "# Contributing to @stencil/store\n\nThank you for your interest in contributing to @stencil/store! This document provides "
},
{
"path": "LICENSE",
"chars": 1090,
"preview": "Copyright 2015-present Drifty Co.\nhttp://drifty.com/\n\nMIT License\n\nPermission is hereby granted, free of charge, to any "
},
{
"path": "README.md",
"chars": 5165,
"preview": "# @stencil/store\n\nStore is a lightweight shared state library by the [StencilJS](https://stenciljs.com/) core team. It i"
},
{
"path": "package.json",
"chars": 1898,
"preview": "{\n \"name\": \"@stencil/store\",\n \"author\": \"StencilJS Team\",\n \"version\": \"2.2.2\",\n \"description\": \"Store is a lightweig"
},
{
"path": "rollup.config.js",
"chars": 366,
"preview": "import typescript from '@rollup/plugin-typescript';\n\nimport pkg from './package.json' with { type: 'json' };\n\nexport def"
},
{
"path": "src/index.test.ts",
"chars": 290,
"preview": "import { describe, it, expect } from 'vitest';\nimport { createStore, createObservableMap } from './index';\n\ndescribe('st"
},
{
"path": "src/index.ts",
"chars": 160,
"preview": "export { createStore } from './store';\nexport { createObservableMap } from './observable-map';\n\n// Types\nexport { Observ"
},
{
"path": "src/observable-map.test.ts",
"chars": 16562,
"preview": "import { describe, expect, test, vi } from 'vitest';\nimport { createObservableMap } from './observable-map';\n\ndescribe.e"
},
{
"path": "src/observable-map.ts",
"chars": 6016,
"preview": "import { OnHandler, OnChangeHandler, Subscription, ObservableMap, Handlers } from './types';\n\ntype Invocable<T> = T | (("
},
{
"path": "src/store.test.ts",
"chars": 1219,
"preview": "import { afterEach, describe, expect, it, vi } from 'vitest';\nimport type { Mock } from 'vitest';\n\nvi.mock('./observable"
},
{
"path": "src/store.ts",
"chars": 461,
"preview": "import { stencilSubscription } from './subscriptions/stencil';\nimport { createObservableMap } from './observable-map';\ni"
},
{
"path": "src/subscriptions/stencil.test.ts",
"chars": 4619,
"preview": "import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nconst coreMock = vi.hoisted(() => ({\n export"
},
{
"path": "src/subscriptions/stencil.ts",
"chars": 2268,
"preview": "import * as StencilCore from '@stencil/core';\nimport { Subscription } from '../types';\nimport { appendToMap, debounce } "
},
{
"path": "src/types.ts",
"chars": 4584,
"preview": "export interface Handlers<T> {\n dispose: DisposeEventHandler[];\n get: GetEventHandler<T>[];\n reset: ResetEventHandler"
},
{
"path": "src/utils.test.ts",
"chars": 2466,
"preview": "import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';\nimport { appendToMap, debounce } from './utils"
},
{
"path": "src/utils.ts",
"chars": 628,
"preview": "export const appendToMap = <K, V extends Object>(map: Map<K, WeakRef<V>[]>, propName: K, value: V) => {\n let refs = map"
},
{
"path": "test-app/.editorconfig",
"chars": 244,
"preview": "# http://editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert"
},
{
"path": "test-app/.gitignore",
"chars": 245,
"preview": "dist/\nwww/\nloader/\n\n*~\n*.sw[mnpcod]\n*.log\n*.lock\n*.tmp\n*.tmp.*\nlog.txt\n*.sublime-project\n*.sublime-workspace\n\n.stencil/\n"
},
{
"path": "test-app/.prettierrc.json",
"chars": 173,
"preview": "{\n \"parser\": \"typescript\",\n \"printWidth\": 100,\n \"semi\": true,\n \"singleQuote\": true,\n \"trailingComma\": \"es5\",\n \"jsx"
},
{
"path": "test-app/LICENSE",
"chars": 1056,
"preview": "MIT License\n\nCopyright (c) 2018\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this so"
},
{
"path": "test-app/package.json",
"chars": 1007,
"preview": "{\n \"name\": \"stencil-store-tests\",\n \"version\": \"0.0.1\",\n \"description\": \"Stencil Component Starter\",\n \"main\": \"./dist"
},
{
"path": "test-app/readme.md",
"chars": 3636,
"preview": " => {\n it('re-renders', async"
},
{
"path": "test-app/src/components/display-store/display-store.tsx",
"chars": 432,
"preview": "import { Component, Prop, h, Host } from '@stencil/core';\nimport { state } from '../../utils/greeting-store';\n\n@Componen"
},
{
"path": "test-app/src/components/simple-store/display-store.spec.ts",
"chars": 2061,
"preview": "import { newSpecPage } from '@stencil/core/testing';\nimport { SimpleStore } from './display-store';\nimport { dispose, re"
},
{
"path": "test-app/src/components/simple-store/display-store.tsx",
"chars": 418,
"preview": "import { Component, h, Host, Method } from '@stencil/core';\nimport { state } from '../../utils/greeting-store';\n\n@Compon"
},
{
"path": "test-app/src/components.d.ts",
"chars": 2265,
"preview": "/* eslint-disable */\n/* tslint:disable */\n/**\n * This is an autogenerated file created by the Stencil compiler.\n * It co"
},
{
"path": "test-app/src/index.html",
"chars": 530,
"preview": "<!DOCTYPE html>\n<html dir=\"ltr\" lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device"
},
{
"path": "test-app/src/index.ts",
"chars": 30,
"preview": "export * from './components';\n"
},
{
"path": "test-app/src/utils/greeting-store.ts",
"chars": 335,
"preview": "import { createStore } from '@stencil/store';\n\nconst store = createStore({\n hello: 'hola',\n goodbye: 'adiós',\n clicks"
},
{
"path": "test-app/stencil.config.ts",
"chars": 285,
"preview": "import { Config } from '@stencil/core';\n\nexport const config: Config = {\n namespace: 'stencil-store-tests',\n outputTar"
},
{
"path": "test-app/tsconfig.json",
"chars": 491,
"preview": "{\n \"compilerOptions\": {\n \"allowSyntheticDefaultImports\": true,\n \"allowUnreachableCode\": false,\n \"declaration\":"
},
{
"path": "tsconfig.json",
"chars": 713,
"preview": "{\n \"compilerOptions\": {\n \"allowSyntheticDefaultImports\": true,\n \"allowUnreachableCode\": false,\n \"declaration\":"
},
{
"path": "vitest.config.ts",
"chars": 448,
"preview": "import { defineConfig } from 'vitest/config';\n\nconst exclude = ['node_modules', 'dist', 'test-app'];\n\nexport default def"
}
]
About this extraction
This page contains the full source code of the ionic-team/stencil-store GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 52 files (94.3 KB), approximately 26.0k tokens, and a symbol index with 42 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.