Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/firebase-deploy-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,8 @@ jobs:
run: firebase experiments:enable webframeworks
- name: Install dependencies
run: npm ci
- name: Install dependencies and build metadata
working-directory: functions/metadata
run: |
npm ci
npm run build
# functions/metadata is a pre-built vendored dist (see functions/metadata/README.md);
# it is installed as a file: dependency by the functions npm ci below.
- name: Create functions environment file
working-directory: functions
run: |
Expand Down
7 changes: 2 additions & 5 deletions .github/workflows/firebase-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,8 @@ jobs:
run: firebase experiments:enable webframeworks
- name: Install dependencies
run: npm ci
- name: Install dependencies and build metadata
working-directory: functions/metadata
run: |
npm ci
npm run build
# functions/metadata is a pre-built vendored dist (see functions/metadata/README.md);
# it is installed as a file: dependency by the functions npm ci below.
- name: Create functions environment file
working-directory: functions
run: |
Expand Down
74 changes: 74 additions & 0 deletions .github/workflows/metadata-drift-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Nudge (never an auto-merge): flags when upstream @jspsych/metadata main has moved past
# the commit DataPipe currently vendors. The vendored copy is pinned + built by
# functions/scripts/sync-metadata.mjs; this job just tells a human it's time to re-sync.
name: Metadata vendor drift check

on:
schedule:
- cron: "0 12 * * 1" # Mondays 12:00 UTC
workflow_dispatch:

permissions:
contents: read
issues: write

jobs:
drift-check:
runs-on: ubuntu-latest
env:
UPSTREAM: jspsych/metadata
ISSUE_TITLE: "[metadata-sync] Vendored @jspsych/metadata is behind upstream main"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6

- name: Compare pinned commit against upstream main
id: compare
run: |
PINNED=$(jq -r .commit functions/metadata/VENDORED_FROM.json)
echo "Pinned commit: $PINNED"
if [ -z "$PINNED" ] || [ "$PINNED" = "null" ]; then
echo "::error::Could not read pinned commit from functions/metadata/VENDORED_FROM.json"
exit 1
fi
UPSTREAM_MAIN=$(gh api "repos/$UPSTREAM/commits/main" --jq .sha)
echo "Upstream main: $UPSTREAM_MAIN"
if [ "$PINNED" = "$UPSTREAM_MAIN" ]; then
echo "in_sync=true" >> "$GITHUB_OUTPUT"
echo "Vendored copy is up to date with upstream main."
exit 0
fi
# ahead_by = how many commits upstream main is ahead of our pinned commit.
AHEAD=$(gh api "repos/$UPSTREAM/compare/$PINNED...main" --jq .ahead_by)
echo "in_sync=false" >> "$GITHUB_OUTPUT"
echo "ahead=$AHEAD" >> "$GITHUB_OUTPUT"
echo "pinned=$PINNED" >> "$GITHUB_OUTPUT"
echo "upstream=$UPSTREAM_MAIN" >> "$GITHUB_OUTPUT"

- name: Open or update tracking issue
if: steps.compare.outputs.in_sync == 'false'
run: |
BODY=$(cat <<EOF
Upstream \`$UPSTREAM\` \`main\` is **${{ steps.compare.outputs.ahead }} commit(s) ahead** of the version DataPipe vendors.

- Pinned: \`${{ steps.compare.outputs.pinned }}\`
- Upstream: \`${{ steps.compare.outputs.upstream }}\`
- Diff: https://github.com/$UPSTREAM/compare/${{ steps.compare.outputs.pinned }}...main

To re-sync (nothing is automatic):
\`\`\`
cd functions && npm run sync:metadata
\`\`\`
Then review the diff, run the tests, and open a PR. See \`functions/metadata/README.md\`.

_This issue is updated automatically by \`.github/workflows/metadata-drift-check.yml\`._
EOF
)
EXISTING=$(gh issue list --state open --search "$ISSUE_TITLE in:title" --json number --jq '.[0].number')
if [ -n "$EXISTING" ]; then
echo "Updating existing issue #$EXISTING"
gh issue edit "$EXISTING" --body "$BODY"
else
echo "Creating new tracking issue"
gh issue create --title "$ISSUE_TITLE" --body "$BODY"
fi
7 changes: 2 additions & 5 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,8 @@ jobs:
- name: Enable firebase webframeworks
run: firebase experiments:enable webframeworks
- run: npm ci
- name: Install dependencies and build metadata
working-directory: functions/metadata
run: |
npm ci
npm run build
# functions/metadata is a pre-built vendored dist (see functions/metadata/README.md);
# it is installed as a file: dependency by the functions npm ci below.
- name: Install dependencies and build functions
working-directory: functions
run: |
Expand Down
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,11 @@ functions/lib/
#vscode local testing
.vscode/settings.json

#metadata build artifacts
functions/metadata/dist
# metadata build artifacts — the vendored @jspsych/metadata dist is intentionally
# COMMITTED (populated by functions/scripts/sync-metadata.mjs) so deploys need no
# metadata build step. See functions/metadata/README.md. These negations override the
# broad `dist` rule above; both the dir and its contents must be re-included.
!functions/metadata/dist/
!functions/metadata/dist/**

CLAUDE.md
21 changes: 21 additions & 0 deletions functions/metadata/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 jspsych

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.
34 changes: 32 additions & 2 deletions functions/metadata/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
# Test utility functions for jsPsych-related test cases
# Vendored @jspsych/metadata (GENERATED — do not hand-edit)

These functions are used to assist in testing jsPsych plugins.
This directory is a **built, committed copy** of `@jspsych/metadata`, produced by
`functions/scripts/sync-metadata.mjs`. `functions/package.json` references it as
`"file:metadata"` and `functions/src` imports `../metadata/dist/index.js` directly, so
deploys need no metadata build step.

## Why vendored instead of an npm dependency?
The version published to npm lags the fixes on the upstream `main` branch (nested
object/array expansion, the `getExtractedArrays`/`getExtractedObjects` APIs, etc.). We
pin to a specific upstream commit and rebuild from it until upstream cuts a fresh release.

## Current pin
- **Package:** @jspsych/metadata 0.0.3
- **Source:** https://github.com/jspsych/metadata.git
- **Commit:** `224d336f8c6e6c67f22e345787f7fd3256bc4cf6` (224d336f8c6e), 2026-06-26T11:29:40-04:00
- **Synced:** 2026-07-01T21:31:30.852Z

(Machine-readable: `VENDORED_FROM.json`.)

## Re-syncing with upstream
```
cd functions
npm run sync:metadata # from upstream main
npm run sync:metadata -- --ref <sha|branch> # from a specific commit
```
Then review the diff, run `npm test` at the repo root, and commit `functions/metadata/`.
A scheduled CI job (`.github/workflows/metadata-drift-check.yml`) flags when upstream main
has moved past this pin.

## Exit plan
When `@jspsych/metadata` ships a released npm version with these fixes, delete this
directory and the sync script, and set the dependency to the published `"^x.y.z"`.
10 changes: 10 additions & 0 deletions functions/metadata/VENDORED_FROM.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"source": "https://github.com/jspsych/metadata.git",
"package": "@jspsych/metadata",
"version": "0.0.3",
"ref": "main",
"commit": "224d336f8c6e6c67f22e345787f7fd3256bc4cf6",
"commitDate": "2026-06-26T11:29:40-04:00",
"syncedAt": "2026-07-01T21:31:30.852Z",
"note": "Generated by functions/scripts/sync-metadata.mjs — do not edit dist/ by hand."
}
70 changes: 70 additions & 0 deletions functions/metadata/dist/AuthorsMap.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Interface that defines the type for the fields that are specified for authors
* according to Psych-DS regulations, with name being the one required field.
*
* @export
* @interface AuthorFields
* @typedef {AuthorFields}
*/
export interface AuthorFields {
/** The type of the author. */
"@type"?: string;
/** The name of the author. (required) */
name: string;
/** The given name of the author. */
givenName?: string;
/** The family name of the author. */
familyName?: string;
/** The identifier that distinguishes the author across datasets (URL). */
identifier?: string;
}
/**
* Class that helps keep track of authors and allows for easy conversion to list format when
* generating the final Metadata file.
*
* @export
* @class AuthorsMap
* @typedef {AuthorsMap}
*/
export declare class AuthorsMap {
/**
* Field that keeps track of the authors in a map.
*
* @private
* @type {({ [key: string]: AuthorFields | string })}
*/
private authors;
/**
* Creates an empty instance of authors map. Doesn't generate default metadata because
* can't assume anything about the authors.
*
* @constructor
*/
constructor();
/**
* Returns the final list format of the authors according to Psych-DS standards.
*
* @returns {(AuthorFields | string)[]} - List of authors
*/
getList(): (AuthorFields | string)[];
/**
* Method that creates an author. This method can also be used to overwrite existing authors
* with the same name in order to update fields.
*
* @param {AuthorFields | string} author - All the required or possible fields associated with listing an author according to Psych-DS standards. Option as a string to define an author according only to name.
*/
setAuthor(author: AuthorFields | string): void;
/**
* Method that fetches an author object allowing user to update (in existing workflow should not be necessary).
*
* @param {string} name - Name of author to be used as key.
* @returns {(AuthorFields | string | {})} - Object with author information. Empty object if not found.
*/
getAuthor(name: string): AuthorFields | string | {};
/**
* Deletes the author if it exists, printing out warning if doesn't exist.
*
* @param {string} author_name - Name of author to be deleted
*/
deleteAuthor(author_name: string): void;
}
109 changes: 109 additions & 0 deletions functions/metadata/dist/PluginCache.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* This class handles the fetching and extraction of description field data about variables
* using plugin and extension type. It caches and parses it efficiently to speed up the metadata generation
* process.
*
* @export
* @class PluginCache
* @typedef {PluginCache}
*/
export declare class PluginCache {
private pluginFields;
constructor();
/**
* Gets the description of a variable in a plugin by fetching the source code of the plugin
* from a remote source (usually unpkg.com) as a string, passing the script to getJsdocsDescription
* to extract the description for the variable (present as JSDoc); caches the result for future use.
*
* @param {string} pluginType - The type of the plugin for which information is to be fetched.
* @param {string} variableName - The name of the variable for which information is to be fetched.
* @param {string} version - The name of the variable for which information is to be fetched.
* @param {boolean} verbose - Indicates whether should run with verbose mode
* @param {boolean} [extension] - An optional flag to indicate if an extension should be used.
* @returns {Promise<string|null>} The description of the plugin variable if found, otherwise null.
* @throws Will throw an error if the fetch operation fails.
*/
getPluginInfo(pluginType: string, variableName: string, version: string, verbose: boolean, extension?: boolean): Promise<any>;
/**
* Method that handles the generation of the fields and calls helpers methods that
* fetch and parse the plugin data.
*
* @private
* @async
* @param {string} pluginType - Name of plugin or extension to fetch.
* @param {string} version - String version to fetch
* @param {boolean} verbose - Boolean indicating verbose mode
* @param {?boolean} [extension] - Optional flag if pluginType is extension
* @returns {unknown}
*/
private generatePluginFields;
/**
* The method that generates the unpkg links based on whether extension vs plugin and the
* specific type.
*
* @private
* @param {string} pluginType - Name of plugin or extension to fetch
* @param {string} version - String version used
* @param {?boolean} [extension] - Optional flag if pluginType is extension
* @returns {string}
*/
private generateUnpkg;
/**
* Fetches the actual script text content from unpkg. Calls the method to generate the link
* and then handles error checking and fetching.
*
* @private
* @async
* @param {string} pluginType - The plugin or extension name to be fetched
* @param {string} version - The string version of the plugin
* @param {boolean} verbose - Boolean indicating verbose mode
* @param {?boolean} [extension] - Whether pluginType is extension
* @returns {unknown}
*/
private fetchScript;
/**
* Extracts the content of the top-level `data: { ... }` block from a jsPsych plugin source
* file using brace counting. This is more robust than a regex approach because the data block
* ends with `},` (not `};`), and plugin sources contain deeply nested objects that would
* cause a lazy regex to stop at the wrong closing brace.
*
* Known limitations (acceptable for current jsPsych plugin sources):
* - Matches the first `data:` property in the file; a plugin with a `data:` field inside its
* `parameters` block before the top-level `info.data` block would extract the wrong object.
* - Brace counting treats every `{`/`}` as structural; braces inside string literals or JSDoc
* comments (e.g. `/** e.g. {foo: 1} *\/`) would throw off the counter.
*
* @private
* @param {string} script - Full plugin source text.
* @returns {string | null} Content between the outer braces of the data block, or null if not found.
*/
private extractDataBlock;
/**
* Parses JSDoc comments and variable blocks from the data section of a jsPsych plugin source.
*
* @private
* @param {string} script - The script text content of the fetching.
* @returns {{}}
*/
private parseJavadocString;
/**
* Extracts JSDoc-annotated fields from a data block string. Uses brace counting to find
* each variable's true closing brace, then recursively processes any `nested:` sub-object
* so that nested parameter descriptions are also captured.
*
* @private
* @param {string} block - Content of a data or nested block (without outer braces).
* @returns {Record<string, any>}
*/
private extractJsdocFields;
/**
* Returns the index of the `}` that closes the `{` at `startIndex`, using brace counting.
* Returns -1 if the source is unbalanced (no matching closing brace found).
*
* @private
* @param {string} str - String to search.
* @param {number} startIndex - Index of the opening `{`.
* @returns {number}
*/
private findMatchingBrace;
}
Loading