gwchq-textjam 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,54 +91,28 @@ Automated unit tests can be run via the `yarn test` command. These unit tests ar
91
91
 
92
92
  ## Publishing to npm
93
93
 
94
- ### Prerequisites
94
+ Publishing is handled by the **"NPM Publish (Scan + Bump + Publish)"** GitHub Actions workflow (`.github/workflows/publish.yml`) — there is no manual `npm publish` step.
95
95
 
96
- 1. Ensure you're logged into npm:
96
+ ### Running the workflow
97
97
 
98
- ```bash
99
- npm login
100
- ```
98
+ 1. Go to the repo's **Actions** tab → **NPM Publish (Scan + Bump + Publish)** → **Run workflow**.
99
+ 2. Fill in the inputs:
100
+ - `git_branch` — the branch to scan, bump, tag, and publish from (defaults to the current release branch).
101
+ - `version_bump_type` — `patch`, `minor`, or `major`, following [semantic versioning](https://semver.org/).
102
+ - `run_publish` — leave checked to publish to npm, or uncheck for a scan + version-bump dry run.
103
+ 3. Run it. The workflow will:
104
+ - Run the shared dependency, secrets, and SAST scans (`security-scan` → `secrets-scan` → `sast-scan`).
105
+ - Run the test suite (`test`).
106
+ - Bump `package.json`, commit, tag (`vX.Y.Z`), and push to `git_branch` (`bump-version`).
107
+ - Build the library (via the `prepublishOnly` hook) and run `npm publish` (`publish`).
101
108
 
102
- 2. Verify you have publish access to the `gwchq-textjam` package.
109
+ Each stage gates the next if a scan or the test suite fails, the version is never bumped and nothing is published.
103
110
 
104
- 3. Make sure all changes are committed and the working directory is clean.
111
+ ### Verify publication
105
112
 
106
- ### Publishing Steps
107
-
108
- 1. **Update the version** in `package.json` following [semantic versioning](https://semver.org/):
109
- - Patch: `0.1.2` → `0.1.3` (bug fixes)
110
- - Minor: `0.1.2` → `0.2.0` (new features, backwards compatible)
111
- - Major: `0.1.2` → `1.0.0` (breaking changes)
112
- 2. **Build the library** (automatically runs via `prepublishOnly` hook):
113
-
114
- ```bash
115
- yarn build:lib
116
- ```
117
-
118
- This creates the `dist` folder with:
119
-
120
- - `index.js` - The main ESM module
121
- - `style.css` - Extracted stylesheet
122
- - `assets/` - Static assets (icons, fonts, etc.)
123
-
124
- 3. **Verify the build output**:
125
-
126
- - Check that `dist/index.js` exists and is properly built
127
- - Verify `dist/style.css` contains all styles
128
- - Ensure all required assets are in `dist/assets/`
129
-
130
- 4. **Publish to npm**:
131
-
132
- ```bash
133
- npm publish
134
- ```
135
-
136
- The `prepublishOnly` script will automatically run `yarn build:lib` before publishing.
137
-
138
- 5. **Verify publication**:
139
- ```bash
140
- npm view gwchq-textjam version
141
- ```
113
+ ```bash
114
+ npm view gwchq-textjam version
115
+ ```
142
116
 
143
117
  ### What Gets Published
144
118
 
package/dist/360.index.js CHANGED
@@ -5,7 +5,7 @@ export const modules = {
5
5
  /***/ 22796:
6
6
  /***/ ((module) => {
7
7
 
8
- module.exports = /*#__PURE__*/JSON.parse('{"modal":{"close":"Close","error":{"error":"Error","heading":"An error has occurred","externalLink":{"message":"Unfortunately links to external sites are not available in the Editor."}}},"editorPanel":{"ariaLabel":"editor text input","characterLimitError":"Error: Character limit reached","characterLimitExplanation_one":"Files in the editor are limited to {{count}} character","characterLimitExplanation_other":"Files in the editor are limited to {{count}} characters","viewOnly":"View only","close":"close"},"filePanel":{"errors":{"reservedFileName":"{{fileName}} is a reserved file name. Please choose a different name.","containsSpaces":"File names must not contain spaces.","generalError":"Error","notUnique":"File names must be unique.","or":"or","unsupportedExtension":"File names must end in {{allowedExtensions}}."},"files":"Project files","images":"Image gallery","newFileButton":"Add file","newFileModal":{"cancel":"Cancel","heading":"Add a new file to your project","helpText":"Remember to add the file extension at the end of your file name, for example, {{examples}}","helpTextExample":{"html":"\'file.html\', \'file.css\' or \'file.js\'","python":"\'file.py\'"},"inputLabel":"Name your file","addFile":"Add file"},"renameFileModal":{"cancel":"Cancel","heading":"Rename file","inputLabel":"Name your file","save":"Save"},"fileMenu":{"label":"Open file menu","renameItem":"Rename file"}},"footer":{"accessibility":"Accessibility","charityNameAndNumber":"Raspberry Pi Foundation UK registered charity 1129409","cookies":"Cookies","privacy":"Privacy","safeguarding":"Safeguarding"},"projectName":{"label":"Project name","newProject":"New Project"},"header":{"download":"Download","editorLogoAltText":"Editor logo","projects":"Your projects","renameProject":"Edit project name","renameSave":"Save project name","save":"Save","loginToSave":"Log in to save","settings":"Settings"},"projectsPanel":{"projects":"Projects","yourProjectsButton":"Go to your projects","projectTypeLabel":"Project type"},"settingsPanel":{"info":"Settings"},"input":{"comment":{"py5":"Py5: imported mode"}},"mobile":{"code":"Code","menu":"Menu","output":"Output","preview":"Preview","steps":"Steps"},"modals":{"close":"Close"},"notifications":{"close":"close","loginPrompt":"To save this project and access it later, don\'t forget to log in or sign up!","projectRenamed":"You have renamed your project.","savePrompt":"Save this project to access it later under \\"Your projects\\"."},"project":{"accessDeniedWithAuthModal":{"embedded":{"text":"Visit the Projects site for cool project ideas"},"heading":"You can\'t access this project","newProject":"Create a new code project","projectsSiteLinkText":"Explore Projects site","text":"Visit the Projects site for cool project ideas or start coding in a new project."},"loading":"Loading","notFoundModal":{"embedded":{"text":"Visit the Projects site for cool project ideas"},"heading":"This project does not exist","newProject":"Start new code project","projectsSiteLinkText":"Explore Projects site","text":"You can start coding in a new project, or visit the Projects site for cool project ideas."},"untitled":"Untitled project"},"projectHeader":{"subTitle":"Code Editor","title":"Your projects","text":"Select a project to continue coding, view, or edit it."},"projectList":{"delete":"Delete","deleteLabel":"Delete project","empty":"No projects created yet","label":"Open project menu","loading":"Loading","loadingFailed":"Failed to load projects","newProject":"Create a new project","pagination":{"first":"First page","last":"Last page","next":"Next page","previous":"Previous page","more":"Load more projects"},"rename":"Rename","renameLabel":"Rename project","renameProjectModal":{"cancel":"Do not save","heading":"Rename project","inputLabel":"Change the name of your project","save":"Save"},"updated":"Edited","python_type":"Python","html_type":"HTML"},"projectTypes":{"html":"HTML/CSS","python":"Python"},"runButton":{"run":"Run","stop":"Stop","stopping":"Stopping..."},"saveStatus":{"saving":"Saving","saved":"Saved"},"sidebar":{"collapse":"Collapse sidebar","download":"Download project","expand":"Expand sidebar","file":"Project files","images":"Image gallery","settings":"Settings","projects":"Projects","information":"Information","information_text":"Our Code Editor is a tool to help young people learn to code. We have only included functions that are simple and safe to use. That\'s why, for example, links to other websites are not allowed.","feedback":"Feedback","help":"Help","privacy":"Privacy","cookies":"Cookies","accessibility":"Accessibility","safeguarding":"Safeguarding","charity":"Raspberry Pi Foundation - UK registered charity 1129409","settingsMenu":{"heading":"Settings","textSize":"Text size","theme":"Theme","textSizeOptions":{"large":"Large","medium":"Medium","small":"Small"},"themeOptions":{"dark":"Dark","light":"Light"}}},"webComponent":{"failed":"Load failed"},"newInputPanelButton":{"buttonText":"Add another panel"},"button":{"yes":"Yes","no":"No"},"common":{"or":"or"}}');
8
+ module.exports = /*#__PURE__*/JSON.parse('{"modal":{"close":"Close","error":{"error":"Error","heading":"An error has occurred","externalLink":{"message":"Unfortunately links to external sites are not available in the Editor."}}},"editorPanel":{"ariaLabel":"editor text input. Press Escape then Tab to move focus out of the editor","characterLimitError":"Error: Character limit reached","characterLimitExplanation_one":"Files in the editor are limited to {{count}} character","characterLimitExplanation_other":"Files in the editor are limited to {{count}} characters","viewOnly":"View only","close":"close"},"filePanel":{"errors":{"reservedFileName":"{{fileName}} is a reserved file name. Please choose a different name.","containsSpaces":"File names must not contain spaces.","generalError":"Error","notUnique":"File names must be unique.","or":"or","unsupportedExtension":"File names must end in {{allowedExtensions}}."},"files":"Project files","images":"Image gallery","newFileButton":"Add file","newFileModal":{"cancel":"Cancel","heading":"Add a new file to your project","helpText":"Remember to add the file extension at the end of your file name, for example, {{examples}}","helpTextExample":{"html":"\'file.html\', \'file.css\' or \'file.js\'","python":"\'file.py\'"},"inputLabel":"Name your file","addFile":"Add file"},"renameFileModal":{"cancel":"Cancel","heading":"Rename file","inputLabel":"Name your file","save":"Save"},"fileMenu":{"label":"Open file menu","renameItem":"Rename file"}},"footer":{"accessibility":"Accessibility","charityNameAndNumber":"Raspberry Pi Foundation UK registered charity 1129409","cookies":"Cookies","privacy":"Privacy","safeguarding":"Safeguarding"},"projectName":{"label":"Project name","newProject":"New Project"},"header":{"download":"Download","editorLogoAltText":"Editor logo","projects":"Your projects","renameProject":"Edit project name","renameSave":"Save project name","save":"Save","loginToSave":"Log in to save","settings":"Settings"},"projectsPanel":{"projects":"Projects","yourProjectsButton":"Go to your projects","projectTypeLabel":"Project type"},"settingsPanel":{"info":"Settings"},"input":{"comment":{"py5":"Py5: imported mode"}},"mobile":{"code":"Code","menu":"Menu","output":"Output","preview":"Preview","steps":"Steps"},"modals":{"close":"Close"},"notifications":{"close":"close","loginPrompt":"To save this project and access it later, don\'t forget to log in or sign up!","projectRenamed":"You have renamed your project.","savePrompt":"Save this project to access it later under \\"Your projects\\"."},"project":{"accessDeniedWithAuthModal":{"embedded":{"text":"Visit the Projects site for cool project ideas"},"heading":"You can\'t access this project","newProject":"Create a new code project","projectsSiteLinkText":"Explore Projects site","text":"Visit the Projects site for cool project ideas or start coding in a new project."},"loading":"Loading","notFoundModal":{"embedded":{"text":"Visit the Projects site for cool project ideas"},"heading":"This project does not exist","newProject":"Start new code project","projectsSiteLinkText":"Explore Projects site","text":"You can start coding in a new project, or visit the Projects site for cool project ideas."},"untitled":"Untitled project"},"projectHeader":{"subTitle":"Code Editor","title":"Your projects","text":"Select a project to continue coding, view, or edit it."},"projectList":{"delete":"Delete","deleteLabel":"Delete project","empty":"No projects created yet","label":"Open project menu","loading":"Loading","loadingFailed":"Failed to load projects","newProject":"Create a new project","pagination":{"first":"First page","last":"Last page","next":"Next page","previous":"Previous page","more":"Load more projects"},"rename":"Rename","renameLabel":"Rename project","renameProjectModal":{"cancel":"Do not save","heading":"Rename project","inputLabel":"Change the name of your project","save":"Save"},"updated":"Edited","python_type":"Python","html_type":"HTML"},"projectTypes":{"html":"HTML/CSS","python":"Python"},"runButton":{"run":"Run","stop":"Stop","stopping":"Stopping..."},"saveStatus":{"saving":"Saving","saved":"Saved"},"sidebar":{"collapse":"Collapse sidebar","download":"Download project","expand":"Expand sidebar","file":"Project files","images":"Image gallery","settings":"Settings","projects":"Projects","information":"Information","information_text":"Our Code Editor is a tool to help young people learn to code. We have only included functions that are simple and safe to use. That\'s why, for example, links to other websites are not allowed.","feedback":"Feedback","help":"Help","privacy":"Privacy","cookies":"Cookies","accessibility":"Accessibility","safeguarding":"Safeguarding","charity":"Raspberry Pi Foundation - UK registered charity 1129409","settingsMenu":{"heading":"Settings","textSize":"Text size","theme":"Theme","textSizeOptions":{"large":"Large","medium":"Medium","small":"Small"},"themeOptions":{"dark":"Dark","light":"Light"}}},"newInputPanelButton":{"buttonText":"Add another panel"},"button":{"yes":"Yes","no":"No"},"common":{"or":"or"},"webComponent":{"failed":"Load failed"}}');
9
9
 
10
10
  /***/ })
11
11
 
@@ -133,6 +133,8 @@ const PyodideWorker = () => {
133
133
  let useSyncXhrStdin = false;
134
134
  /** True when current input request was cancelled from outside the worker. */
135
135
  let stdinCancelled = false;
136
+ /** True when the sync-XHR stdin transport failed (SW unreachable / request escaped). */
137
+ let stdinTransportFailed = false;
136
138
  /** Used when SharedArrayBuffer is unavailable: resolve for the pending input() call. */
137
139
  let pendingStdinResolve = null;
138
140
  // Until Pyodide is fully initialised, keep stdout/stderr in the dev console only.
@@ -185,6 +187,7 @@ const PyodideWorker = () => {
185
187
  case "runPython":
186
188
  currentRunId = data.runId || null;
187
189
  stdinCancelled = false;
190
+ stdinTransportFailed = false;
188
191
  runPython(data.python, data.userModuleNames);
189
192
  break;
190
193
  case "stopPython": {
@@ -238,10 +241,7 @@ const PyodideWorker = () => {
238
241
  await runUserCode();
239
242
  });
240
243
  } catch (error) {
241
- const isStdinControlError =
242
- error?.message === "PYODIDE_STDIN_CANCELLED" ||
243
- error?.message === "PYODIDE_STDIN_TIMEOUT" ||
244
- error?.message === "PYODIDE_STDIN_ABORTED";
244
+ const isStdinControlError = error?.message === "PYODIDE_STDIN_CANCELLED";
245
245
 
246
246
  if (stdinCancelled || isStdinControlError) {
247
247
  postMessage({
@@ -256,6 +256,19 @@ const PyodideWorker = () => {
256
256
  return;
257
257
  }
258
258
 
259
+ if (stdinTransportFailed) {
260
+ postMessage({
261
+ method: "handleError",
262
+ file: "main.py",
263
+ line: "",
264
+ mistake: "",
265
+ type: "InputUnavailableError",
266
+ info: "Input timed out. Rerun your code and try again.",
267
+ });
268
+ await clearPyodideData(userModuleNames);
269
+ return;
270
+ }
271
+
259
272
  if (!(error instanceof pyodide.ffi.PythonError)) {
260
273
  postMessage({
261
274
  method: "handleError",
@@ -691,59 +704,64 @@ const PyodideWorker = () => {
691
704
  };
692
705
 
693
706
  const readInputViaSyncXhr = (runId) => {
694
- const requestId = crypto.randomUUID();
695
- const requestUrl = new URL(
696
- stdinFallbackConfig.endpointPath,
697
- // eslint-disable-next-line no-restricted-globals
698
- self.location.origin,
699
- );
700
- requestUrl.searchParams.set("runId", runId || "");
701
- requestUrl.searchParams.set("requestId", requestId);
702
- requestUrl.searchParams.set("clientId", stdinFallbackConfig.clientId);
703
-
704
- const xhr = new XMLHttpRequest();
705
- xhr.open("GET", requestUrl.toString(), false);
706
- xhr.setRequestHeader("X-Pyodide-Stdin-Request", "true");
707
- xhr.setRequestHeader("Cache-Control", "no-store");
708
-
707
+ // Mark any failure with XHR setup as stdinTransportFailed for a clean error
709
708
  try {
709
+ const requestId = crypto.randomUUID();
710
+ const requestUrl = new URL(
711
+ stdinFallbackConfig.endpointPath,
712
+ // eslint-disable-next-line no-restricted-globals
713
+ self.location.origin,
714
+ );
715
+ requestUrl.searchParams.set("runId", runId || "");
716
+ requestUrl.searchParams.set("requestId", requestId);
717
+ requestUrl.searchParams.set("clientId", stdinFallbackConfig.clientId);
718
+
719
+ const xhr = new XMLHttpRequest();
720
+ xhr.open("GET", requestUrl.toString(), false);
721
+ xhr.setRequestHeader("X-Pyodide-Stdin-Request", "true");
722
+ xhr.setRequestHeader("Cache-Control", "no-store");
710
723
  xhr.send(null);
711
- } catch (_) {
712
- throw new Error("Failed to read Python input via stdin fallback");
713
- }
714
724
 
715
- if (xhr.status === STDIN_CANCELLED_RESPONSE_CODE) {
716
- stdinCancelled = true;
717
- throw new Error("PYODIDE_STDIN_CANCELLED");
718
- }
725
+ if (xhr.status === STDIN_CANCELLED_RESPONSE_CODE) {
726
+ stdinCancelled = true;
727
+ throw new Error("PYODIDE_STDIN_CANCELLED");
728
+ }
719
729
 
720
- if (xhr.status === STDIN_TIMEOUT_RESPONSE_CODE) {
721
- throw new Error("PYODIDE_STDIN_TIMEOUT");
722
- }
730
+ if (xhr.status === STDIN_TIMEOUT_RESPONSE_CODE) {
731
+ stdinTransportFailed = true;
732
+ throw new Error("PYODIDE_STDIN_TIMEOUT");
733
+ }
723
734
 
724
- if (xhr.status === STDIN_ABORTED_RESPONSE_CODE) {
725
- throw new Error("PYODIDE_STDIN_ABORTED");
726
- }
735
+ if (xhr.status === STDIN_ABORTED_RESPONSE_CODE) {
736
+ stdinTransportFailed = true;
737
+ throw new Error("PYODIDE_STDIN_ABORTED");
738
+ }
727
739
 
728
- // EOF: orchestrator signalled Ctrl+D / no more input. Returning null tells
729
- // Pyodide's line-based stdin handler to raise EOFError at the input() call
730
- // site, rather than escalating to a KeyboardInterrupt that tears down the run.
731
- if (xhr.status === STDIN_EOF_RESPONSE_CODE) {
732
- return null;
733
- }
740
+ // EOF: orchestrator signalled Ctrl+D / no more input. Returning null tells
741
+ // Pyodide's line-based stdin handler to raise EOFError at the input() call
742
+ // site, rather than escalating to a KeyboardInterrupt that tears down the run.
743
+ if (xhr.status === STDIN_EOF_RESPONSE_CODE) {
744
+ return null;
745
+ }
734
746
 
735
- if (xhr.status !== 200) {
736
- throw new Error(
737
- `Python input request failed with status ${xhr.status}: ${xhr.responseText}`,
738
- );
739
- }
747
+ if (xhr.status !== 200) {
748
+ throw new Error(
749
+ `Python input request failed with status ${xhr.status}: ${xhr.responseText}`,
750
+ );
751
+ }
740
752
 
741
- let content = xhr.responseText ?? "";
742
- if (!content.endsWith("\n")) {
743
- content += "\n";
744
- }
753
+ let content = xhr.responseText ?? "";
754
+ if (!content.endsWith("\n")) {
755
+ content += "\n";
756
+ }
745
757
 
746
- return content;
758
+ return content;
759
+ } catch (err) {
760
+ if (!stdinCancelled) {
761
+ stdinTransportFailed = true;
762
+ }
763
+ throw err;
764
+ }
747
765
  };
748
766
 
749
767
  const clearPyodideData = async (userModuleNames) => {
@@ -175,6 +175,13 @@ self.addEventListener("message", (event) => {
175
175
  event.source && "id" in event.source ? event.source.id : "";
176
176
  const clientId = data.clientId || eventClientId || "";
177
177
 
178
+ if (data.type === "PYODIDE_STDIN_PROBE") {
179
+ if (event.source) {
180
+ event.source.postMessage({ type: "PYODIDE_STDIN_PROBE_ACK" });
181
+ }
182
+ return;
183
+ }
184
+
178
185
  if (data.type === "PYODIDE_STDIN_REGISTER") {
179
186
  if (data.clientId && eventClientId) {
180
187
  tabClientToWindowClient.set(String(data.clientId), String(eventClientId));