@scratch/scratch-vm 13.7.2 → 13.7.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scratch/scratch-vm",
3
- "version": "13.7.2",
3
+ "version": "13.7.3",
4
4
  "description": "Virtual Machine for Scratch 3.0",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/scratchfoundation/scratch-vm#readme",
@@ -60,8 +60,8 @@
60
60
  "allow-incomplete-coverage": true
61
61
  },
62
62
  "dependencies": {
63
- "@scratch/scratch-render": "13.7.2",
64
- "@scratch/scratch-svg-renderer": "13.7.2",
63
+ "@scratch/scratch-render": "13.7.3",
64
+ "@scratch/scratch-svg-renderer": "13.7.3",
65
65
  "@vernier/godirect": "1.8.3",
66
66
  "arraybuffer-loader": "1.0.8",
67
67
  "atob": "2.1.2",
@@ -654,52 +654,38 @@ class Target extends EventEmitter {
654
654
  }
655
655
 
656
656
  /**
657
- * Fixes up variable references in this target avoiding conflicts with
658
- * pre-existing variables in the same scope.
659
- * This is used when uploading this target as a new sprite into an existing
660
- * project, where the new sprite may contain references
661
- * to variable names that already exist as global variables in the project
662
- * (and thus are in scope for variable references in the given sprite).
657
+ * Reconciles variable, list, and broadcast references on this target against
658
+ * the variables actually defined in the project, creating definitions on the
659
+ * stage for any references whose ids are not defined anywhere. Does not
660
+ * rename any existing variables.
663
661
  *
664
- * If this target has a block that references an existing global variable and that
665
- * variable *does not* exist in this target (e.g. it was a global variable in the
666
- * project the sprite was originally exported from), merge the variables. This entails
667
- * fixing the variable references in this sprite to reference the id of the pre-existing global variable.
662
+ * For each variable, list, or broadcast referenced by a block on this target:
663
+ * - If the referenced id is found locally on this sprite or on the stage,
664
+ * the reference is already correct and nothing happens.
665
+ * - If the referenced id is not defined anywhere, the stage is checked for a
666
+ * variable with the same name and type. If one exists, the field id is
667
+ * remapped to that existing global. Otherwise, a new global is created on
668
+ * the stage with a non-conflicting name and the field name is updated.
668
669
  *
669
- * If this target has a block that references an existing global variable and that
670
- * variable does exist in the target itself (e.g. it's a local variable in the sprite being uploaded),
671
- * then the local variable is renamed to distinguish itself from the pre-existing variable.
672
- * All blocks that reference the local variable will be updated to use the new name.
670
+ * Used during whole-project load to repair projects corrupted by historical
671
+ * bugs that left dangling references, and as the definition-creation phase
672
+ * of `fixUpVariableReferences` for sprite import and backpack paste.
673
673
  */
674
- // TODO (#1360) This function is too long, add some helpers for the different chunks and cases...
675
- fixUpVariableReferences () {
676
- if (!this.runtime) return; // There's no runtime context to conflict with
677
- if (this.isStage) return; // Stage can't have variable conflicts with itself (and also can't be uploaded)
674
+ reconcileVariableReferences () {
675
+ if (!this.runtime) return;
678
676
  const stage = this.runtime.getTargetForStage();
679
677
  if (!stage || !stage.variables) return;
680
678
 
681
- const renameConflictingLocalVar = (id, name, type) => {
682
- const conflict = stage.lookupVariableByNameAndType(name, type);
683
- if (conflict) {
684
- const newName = StringUtil.unusedName(
685
- `${this.getName()}: ${name}`,
686
- this.getAllVariableNamesInScopeByType(type));
687
- this.renameVariable(id, newName);
688
- return newName;
689
- }
690
- return null;
691
- };
692
-
693
- const allReferences = this.blocks.getAllVariableAndListReferences();
694
- const unreferencedLocalVarIds = [];
695
- if (Object.keys(this.variables).length > 0) {
696
- for (const localVarId in this.variables) {
697
- if (!Object.prototype.hasOwnProperty.call(this.variables, localVarId)) continue;
698
- if (!allReferences[localVarId]) unreferencedLocalVarIds.push(localVarId);
699
- }
700
- }
679
+ const allReferences = this.blocks.getAllVariableAndListReferences(null, true);
701
680
  const conflictIdsToReplace = Object.create(null);
702
681
  const conflictNamesToReplace = Object.create(null);
682
+ // When a dangling reference triggers creation of a new stage variable, remember
683
+ // the original (pre-bump) name so subsequent dangling references with the same
684
+ // original name and type coalesce to the same stage variable instead of creating
685
+ // a second one. Scratchers who pasted scripts referencing what they called
686
+ // "score" twice almost certainly meant one variable, not two.
687
+ const createdForOriginalName = Object.create(null);
688
+ const originalNameKey = (name, type) => `${type}\u0000${name}`;
703
689
 
704
690
  // Cache the list of all variable names by type so that we don't need to
705
691
  // re-calculate this in every iteration of the following loop.
@@ -712,77 +698,92 @@ class Target extends EventEmitter {
712
698
  };
713
699
 
714
700
  for (const varId in allReferences) {
715
- // We don't care about which var ref we get, they should all have the same var info
701
+ const existing = this.lookupVariableById(varId);
702
+ if (existing) {
703
+ // The id resolves to a real variable. Normalize displayed names so
704
+ // every block field shows the variable's current name. A previous
705
+ // target's pass on this load may have created the stage variable with
706
+ // a bumped name; refs on this target that still show the original
707
+ // name need to be brought into agreement. Scan all refs (not just
708
+ // the first) so an inconsistent set of refs to the same id heals
709
+ // even when the first ref happens to already match.
710
+ if (!conflictNamesToReplace[varId]) {
711
+ const staleRef = allReferences[varId].find(
712
+ ref => ref.referencingField.value !== existing.name
713
+ );
714
+ if (staleRef) {
715
+ conflictNamesToReplace[varId] = existing.name;
716
+ log.warn(
717
+ `Reconciled stale displayed name on '${this.getName()}': updated to ` +
718
+ `'${existing.name}' for id '${varId}' ` +
719
+ `(was '${staleRef.referencingField.value}').`
720
+ );
721
+ }
722
+ }
723
+ continue;
724
+ }
725
+ // The referenced id is not defined anywhere. Treat this as a reference
726
+ // to a global from a different project (or from a backpack paste / sprite
727
+ // import that lost its definition). Look for a same-name same-type global
728
+ // on the stage; if found, queue an id remap, otherwise create a fresh one.
716
729
  const varRef = allReferences[varId][0];
717
730
  const varName = varRef.referencingField.value;
718
731
  const varType = varRef.type;
719
- if (this.lookupVariableById(varId)) {
720
- // Found a variable with the id in either the target or the stage,
721
- // figure out which one.
722
- if (Object.prototype.hasOwnProperty.call(this.variables, varId)) {
723
- // If the target has the variable, then check whether the stage
724
- // has one with the same name and type. If it does, then rename
725
- // this target specific variable so that there is a distinction.
726
- const newVarName = renameConflictingLocalVar(varId, varName, varType);
727
-
728
- if (newVarName) {
729
- // We are not calling this.blocks.updateBlocksAfterVarRename
730
- // here because it will search through all the blocks. We already
731
- // have access to all the references for this var id.
732
- allReferences[varId].map(ref => {
733
- ref.referencingField.value = newVarName;
734
- return ref;
735
- });
736
- }
732
+ const existingVar = stage.lookupVariableByNameAndType(varName, varType);
733
+ if (existingVar) {
734
+ if (!conflictIdsToReplace[varId]) {
735
+ conflictIdsToReplace[varId] = existingVar.id;
736
+ log.warn(
737
+ `Reconciled dangling reference on '${this.getName()}': remapped id '${varId}' ` +
738
+ `(name '${varName}', type '${varType}') to existing stage variable '${existingVar.id}'.`
739
+ );
737
740
  }
738
741
  } else {
739
- // We didn't find the referenced variable id anywhere,
740
- // Treat it as a reference to a global variable (from the original
741
- // project this sprite was exported from).
742
- // Check for whether a global variable of the same name and type exists,
743
- // and if so, track it to merge with the existing global in a second pass of the blocks.
744
- const existingVar = stage.lookupVariableByNameAndType(varName, varType);
745
- if (existingVar) {
742
+ const coalesceKey = originalNameKey(varName, varType);
743
+ const earlierCreated = createdForOriginalName[coalesceKey];
744
+ if (earlierCreated) {
745
+ // An earlier dangling reference in this pass already triggered creation
746
+ // for this original name and type. Coalesce to that stage variable, and
747
+ // update this reference's displayed name to match the bumped name so the
748
+ // two blocks display consistently.
746
749
  if (!conflictIdsToReplace[varId]) {
747
- conflictIdsToReplace[varId] = existingVar.id;
750
+ conflictIdsToReplace[varId] = earlierCreated.id;
751
+ conflictNamesToReplace[varId] = earlierCreated.freshName;
752
+ log.warn(
753
+ `Reconciled dangling reference on '${this.getName()}': coalesced id '${varId}' ` +
754
+ `(name '${varName}', type '${varType}') with earlier-created stage variable ` +
755
+ `'${earlierCreated.id}' (name '${earlierCreated.freshName}').`
756
+ );
748
757
  }
749
758
  } else {
750
- // A global variable with the same name did not already exist,
751
- // create a new one such that it does not conflict with any
752
- // names of local variables of the same type.
753
759
  const allNames = allVarNames(varType);
754
760
  const freshName = StringUtil.unusedName(varName, allNames);
755
761
  stage.createVariable(varId, freshName, varType);
762
+ // Track the new name so unusedName accounts for it on subsequent calls,
763
+ // and remember which stage variable served this original name so
764
+ // future same-name dangling references coalesce instead of duplicating.
765
+ allNames.push(freshName);
766
+ createdForOriginalName[coalesceKey] = {id: varId, freshName};
756
767
  if (!conflictNamesToReplace[varId]) {
757
768
  conflictNamesToReplace[varId] = freshName;
769
+ log.warn(
770
+ `Reconciled dangling reference on '${this.getName()}': created stage variable ` +
771
+ `'${varId}' (name '${freshName}', type '${varType}').`
772
+ );
758
773
  }
759
774
  }
760
775
  }
761
776
  }
762
- // Rename any local variables that were missed above because they aren't
763
- // referenced by any blocks
764
- for (const id in unreferencedLocalVarIds) {
765
- const varId = unreferencedLocalVarIds[id];
766
- const name = this.variables[varId].name;
767
- const type = this.variables[varId].type;
768
- renameConflictingLocalVar(varId, name, type);
769
- }
770
- // Handle global var conflicts with existing global vars (e.g. a sprite is uploaded, and has
771
- // blocks referencing some variable that the sprite does not own, and this
772
- // variable conflicts with a global var)
773
- // In this case, we want to merge the new variable referenes with the
774
- // existing global variable
777
+
778
+ // Apply queued id remaps (merge the dangling references with the existing global).
775
779
  for (const conflictId in conflictIdsToReplace) {
776
780
  const existingId = conflictIdsToReplace[conflictId];
777
781
  const referencesToUpdate = allReferences[conflictId];
778
782
  this.mergeVariables(conflictId, existingId, referencesToUpdate);
779
783
  }
780
784
 
781
- // Handle global var conflicts existing local vars (e.g a sprite is uploaded,
782
- // and has blocks referencing some variable that the sprite does not own, and this
783
- // variable conflcits with another sprite's local var).
784
- // In this case, we want to go through the variable references and update
785
- // the name of the variable in that reference.
785
+ // Apply queued field-name updates for newly-created globals whose name was
786
+ // bumped to avoid a collision.
786
787
  for (const conflictId in conflictNamesToReplace) {
787
788
  const newName = conflictNamesToReplace[conflictId];
788
789
  const referencesToUpdate = allReferences[conflictId];
@@ -793,6 +794,66 @@ class Target extends EventEmitter {
793
794
  }
794
795
  }
795
796
 
797
+ /**
798
+ * Reconciles missing definitions (via `reconcileVariableReferences`) and then
799
+ * renames sprite-local variables that name-collide with stage globals so they
800
+ * remain distinguishable after import.
801
+ *
802
+ * Used when importing a sprite into an existing project and when pasting
803
+ * blocks from the backpack. Project load uses `reconcileVariableReferences`
804
+ * directly so that legitimately-existing local-vs-global name collisions in
805
+ * a saved project are not renamed.
806
+ */
807
+ fixUpVariableReferences () {
808
+ if (!this.runtime) return;
809
+ const stage = this.runtime.getTargetForStage();
810
+ if (!stage || !stage.variables) return;
811
+
812
+ // First, ensure every referenced variable, list, or broadcast has a definition.
813
+ this.reconcileVariableReferences();
814
+
815
+ // The stage's variables are the global scope; there's no local-vs-global
816
+ // distinction to disambiguate.
817
+ if (this.isStage) return;
818
+
819
+ const renameConflictingLocalVar = (id, name, type) => {
820
+ const conflict = stage.lookupVariableByNameAndType(name, type);
821
+ if (conflict) {
822
+ const newName = StringUtil.unusedName(
823
+ `${this.getName()}: ${name}`,
824
+ this.getAllVariableNamesInScopeByType(type));
825
+ this.renameVariable(id, newName);
826
+ return newName;
827
+ }
828
+ return null;
829
+ };
830
+
831
+ // Walk the references again and rename any sprite-local variables whose
832
+ // name and type collide with a stage global. References on this target
833
+ // that point at the local are updated to use the new name.
834
+ const allReferences = this.blocks.getAllVariableAndListReferences(null, true);
835
+ for (const varId in allReferences) {
836
+ if (!Object.prototype.hasOwnProperty.call(this.variables, varId)) continue;
837
+ const varRef = allReferences[varId][0];
838
+ const newVarName = renameConflictingLocalVar(varId, varRef.referencingField.value, varRef.type);
839
+ if (newVarName) {
840
+ allReferences[varId].map(ref => {
841
+ ref.referencingField.value = newVarName;
842
+ return ref;
843
+ });
844
+ }
845
+ }
846
+
847
+ // Rename any local variables that aren't referenced by any block but still
848
+ // collide with a stage global, so the sprite remains internally consistent.
849
+ for (const localVarId in this.variables) {
850
+ if (!Object.prototype.hasOwnProperty.call(this.variables, localVarId)) continue;
851
+ if (allReferences[localVarId]) continue;
852
+ const v = this.variables[localVarId];
853
+ renameConflictingLocalVar(localVarId, v.name, v.type);
854
+ }
855
+ }
856
+
796
857
  }
797
858
 
798
859
  module.exports = Target;
@@ -560,7 +560,13 @@ class VirtualMachine extends EventEmitter {
560
560
  this.editingTarget = targets[0];
561
561
  }
562
562
 
563
- if (!wholeProject) {
563
+ if (wholeProject) {
564
+ // A loaded project may carry dangling variable, list, or broadcast
565
+ // references baked in by historical bugs. Reconcile each target so
566
+ // those references resolve cleanly without renaming any legitimate
567
+ // local-vs-global name collisions.
568
+ targets.forEach(target => target.reconcileVariableReferences());
569
+ } else {
564
570
  this.editingTarget.fixUpVariableReferences();
565
571
  }
566
572
 
@@ -1280,6 +1286,12 @@ class VirtualMachine extends EventEmitter {
1280
1286
  target.blocks.createBlock(block);
1281
1287
  });
1282
1288
  target.blocks.updateTargetSpecificBlocks(target.isStage);
1289
+ if (!optFromTargetId) {
1290
+ // No source target means the blocks come from outside the project (e.g. the
1291
+ // backpack). Reconcile any variable, list, or broadcast references against
1292
+ // what's defined in the project, creating missing definitions on the stage.
1293
+ target.fixUpVariableReferences();
1294
+ }
1283
1295
  });
1284
1296
  }
1285
1297