Skip to content

Comments

feat: nativeMagicString slice#7807

Merged
IWANABETHATGUY merged 1 commit intomainfrom
01-08-feat_nativemagicstring_slice
Jan 9, 2026
Merged

feat: nativeMagicString slice#7807
IWANABETHATGUY merged 1 commit intomainfrom
01-08-feat_nativemagicstring_slice

Conversation

@IWANABETHATGUY
Copy link
Member

No description provided.

Copy link
Member Author

IWANABETHATGUY commented Jan 8, 2026


How to use the Graphite Merge Queue

Add the label graphite: merge-when-ready to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@netlify
Copy link

netlify bot commented Jan 8, 2026

Deploy Preview for rolldown-rs canceled.

Name Link
🔨 Latest commit 1960898
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/6960b82698d6fd000835d4b3

@IWANABETHATGUY IWANABETHATGUY force-pushed the 01-08-feat_nativemagicstring_slice branch from 4874141 to 8d38e99 Compare January 9, 2026 06:47
@IWANABETHATGUY IWANABETHATGUY force-pushed the 01-08-feat_nativemagicstring_slice branch from e5b2103 to 25f65a4 Compare January 9, 2026 06:59
@IWANABETHATGUY IWANABETHATGUY marked this pull request as ready for review January 9, 2026 07:20
Copilot AI review requested due to automatic review settings January 9, 2026 07:20
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements the slice method for the MagicString class, which returns the generated content between specified original character positions. The implementation adds native Rust support for this functionality that was previously unavailable.

  • Implements the core slice algorithm in Rust that handles character ranges, negative indices, and intro/outro content
  • Adds JavaScript/TypeScript bindings with proper character-to-byte index conversion for UTF-16/UTF-8 interop
  • Enables several slice tests while documenting specific test skips for known limitations (overlapping overwrites, move operations)

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
crates/string_wizard/src/magic_string/slice.rs Core Rust implementation of the slice algorithm with negative index handling and error checking for replaced characters
crates/string_wizard/src/magic_string/mod.rs Adds slice module to the public API
crates/rolldown_binding/src/types/binding_magic_string.rs JavaScript binding layer with character-to-byte index conversion and NAPI integration
packages/rolldown/src/binding.d.cts TypeScript type definitions for the new slice method
packages/rolldown/tests/magic-string/download-tests.mjs Removes slice from skipped test suites and documents specific test skips
packages/rolldown/tests/magic-string/MagicString.test.ts Enables slice test suite with selective skips for complex move interactions
Comments suppressed due to low confidence (2)

packages/rolldown/tests/magic-string/MagicString.test.ts:1589

  • There is no test coverage for the case where start and end are equal (e.g., slice(5, 5)), which should return an empty string. Consider adding a test case to verify this edge case behaves correctly.
  describe('slice', () => {
    it.skip('should return the generated content between the specified original characters', () => {
      const s = new MagicString('abcdefghijkl');

      assert.equal(s.slice(3, 9), 'defghi');
      s.overwrite(4, 8, 'XX');
      assert.equal(s.slice(3, 9), 'dXXi');
      s.overwrite(2, 10, 'ZZ');
      assert.equal(s.slice(1, 11), 'bZZk');
      assert.equal(s.slice(2, 10), 'ZZ');

      assert.throws(() => s.slice(3, 9));
    });

    it('defaults `end` to the original string length', () => {
      const s = new MagicString('abcdefghijkl');
      assert.equal(s.slice(3), 'defghijkl');
    });

    it('allows negative numbers as arguments', () => {
      const s = new MagicString('abcdefghijkl');
      assert.equal(s.slice(-3), 'jkl');
      assert.equal(s.slice(0, -3), 'abcdefghi');
    });

    it('includes inserted characters, respecting insertion direction', () => {
      const s = new MagicString('abefij');

      s.prependRight(2, 'cd');
      s.appendLeft(4, 'gh');

      assert.equal(s.slice(), 'abcdefghij');
      assert.equal(s.slice(1, 5), 'bcdefghi');
      assert.equal(s.slice(2, 4), 'cdefgh');
      assert.equal(s.slice(3, 4), 'fgh');
      assert.equal(s.slice(0, 2), 'ab');
      assert.equal(s.slice(0, 3), 'abcde');
      assert.equal(s.slice(4, 6), 'ij');
      assert.equal(s.slice(3, 6), 'fghij');
    });

    it.skip('supports characters moved outward', () => {
      const s = new MagicString('abcdEFghIJklmn');

      s.move(4, 6, 2);
      s.move(8, 10, 12);
      assert.equal(s.toString(), 'abEFcdghklIJmn');

      assert.equal(s.slice(1, -1), 'bEFcdghklIJm');
      assert.equal(s.slice(2, -2), 'cdghkl');
      assert.equal(s.slice(3, -3), 'dghk');
      assert.equal(s.slice(4, -4), 'EFcdghklIJ');
      assert.equal(s.slice(5, -5), 'FcdghklI');
      assert.equal(s.slice(6, -6), 'gh');
    });

    it.skip('supports characters moved inward', () => {
      const s = new MagicString('abCDefghijKLmn');

      s.move(2, 4, 6);
      s.move(10, 12, 8);
      assert.equal(s.toString(), 'abefCDghKLijmn');

      assert.equal(s.slice(1, -1), 'befCDghKLijm');
      assert.equal(s.slice(2, -2), 'CDghKL');
      assert.equal(s.slice(3, -3), 'DghK');
      assert.equal(s.slice(4, -4), 'efCDghKLij');
      assert.equal(s.slice(5, -5), 'fCDghKLi');
      assert.equal(s.slice(6, -6), 'gh');
    });

    it.skip('supports characters moved opposing', () => {
      const s = new MagicString('abCDefghIJkl');

      s.move(2, 4, 8);
      s.move(8, 10, 4);
      assert.equal(s.toString(), 'abIJefghCDkl');

      assert.equal(s.slice(1, -1), 'bIJefghCDk');
      assert.equal(s.slice(2, -2), '');
      assert.equal(s.slice(3, -3), '');
      assert.equal(s.slice(-3, 3), 'JefghC');
      assert.equal(s.slice(4, -4), 'efgh');
      assert.equal(s.slice(0, 3), 'abIJefghC');
      assert.equal(s.slice(3), 'Dkl');
      assert.equal(s.slice(0, -3), 'abI');
      assert.equal(s.slice(-3), 'JefghCDkl');
    });

    it('errors if replaced characters are used as slice anchors', () => {
      const s = new MagicString('abcdef');
      s.overwrite(2, 4, 'CD');

      assert.throws(() => s.slice(2, 3), /slice end anchor/);

      assert.throws(() => s.slice(3, 4), /slice start anchor/);

      assert.throws(() => s.slice(3, 5), /slice start anchor/);

      assert.equal(s.slice(1, 5), 'bCDe');
    });

    it('does not error if slice is after removed characters', () => {
      const s = new MagicString('abcdef');

      s.remove(0, 2);

      assert.equal(s.slice(2, 4), 'cd');
    });
  });

packages/rolldown/tests/magic-string/MagicString.test.ts:1589

  • There is no test coverage for the case where both start and end are negative and result in an empty range (e.g., slice(-3, -5) which has start > end after normalization). Consider adding a test case to verify this edge case returns an empty string as expected.
  describe('slice', () => {
    it.skip('should return the generated content between the specified original characters', () => {
      const s = new MagicString('abcdefghijkl');

      assert.equal(s.slice(3, 9), 'defghi');
      s.overwrite(4, 8, 'XX');
      assert.equal(s.slice(3, 9), 'dXXi');
      s.overwrite(2, 10, 'ZZ');
      assert.equal(s.slice(1, 11), 'bZZk');
      assert.equal(s.slice(2, 10), 'ZZ');

      assert.throws(() => s.slice(3, 9));
    });

    it('defaults `end` to the original string length', () => {
      const s = new MagicString('abcdefghijkl');
      assert.equal(s.slice(3), 'defghijkl');
    });

    it('allows negative numbers as arguments', () => {
      const s = new MagicString('abcdefghijkl');
      assert.equal(s.slice(-3), 'jkl');
      assert.equal(s.slice(0, -3), 'abcdefghi');
    });

    it('includes inserted characters, respecting insertion direction', () => {
      const s = new MagicString('abefij');

      s.prependRight(2, 'cd');
      s.appendLeft(4, 'gh');

      assert.equal(s.slice(), 'abcdefghij');
      assert.equal(s.slice(1, 5), 'bcdefghi');
      assert.equal(s.slice(2, 4), 'cdefgh');
      assert.equal(s.slice(3, 4), 'fgh');
      assert.equal(s.slice(0, 2), 'ab');
      assert.equal(s.slice(0, 3), 'abcde');
      assert.equal(s.slice(4, 6), 'ij');
      assert.equal(s.slice(3, 6), 'fghij');
    });

    it.skip('supports characters moved outward', () => {
      const s = new MagicString('abcdEFghIJklmn');

      s.move(4, 6, 2);
      s.move(8, 10, 12);
      assert.equal(s.toString(), 'abEFcdghklIJmn');

      assert.equal(s.slice(1, -1), 'bEFcdghklIJm');
      assert.equal(s.slice(2, -2), 'cdghkl');
      assert.equal(s.slice(3, -3), 'dghk');
      assert.equal(s.slice(4, -4), 'EFcdghklIJ');
      assert.equal(s.slice(5, -5), 'FcdghklI');
      assert.equal(s.slice(6, -6), 'gh');
    });

    it.skip('supports characters moved inward', () => {
      const s = new MagicString('abCDefghijKLmn');

      s.move(2, 4, 6);
      s.move(10, 12, 8);
      assert.equal(s.toString(), 'abefCDghKLijmn');

      assert.equal(s.slice(1, -1), 'befCDghKLijm');
      assert.equal(s.slice(2, -2), 'CDghKL');
      assert.equal(s.slice(3, -3), 'DghK');
      assert.equal(s.slice(4, -4), 'efCDghKLij');
      assert.equal(s.slice(5, -5), 'fCDghKLi');
      assert.equal(s.slice(6, -6), 'gh');
    });

    it.skip('supports characters moved opposing', () => {
      const s = new MagicString('abCDefghIJkl');

      s.move(2, 4, 8);
      s.move(8, 10, 4);
      assert.equal(s.toString(), 'abIJefghCDkl');

      assert.equal(s.slice(1, -1), 'bIJefghCDk');
      assert.equal(s.slice(2, -2), '');
      assert.equal(s.slice(3, -3), '');
      assert.equal(s.slice(-3, 3), 'JefghC');
      assert.equal(s.slice(4, -4), 'efgh');
      assert.equal(s.slice(0, 3), 'abIJefghC');
      assert.equal(s.slice(3), 'Dkl');
      assert.equal(s.slice(0, -3), 'abI');
      assert.equal(s.slice(-3), 'JefghCDkl');
    });

    it('errors if replaced characters are used as slice anchors', () => {
      const s = new MagicString('abcdef');
      s.overwrite(2, 4, 'CD');

      assert.throws(() => s.slice(2, 3), /slice end anchor/);

      assert.throws(() => s.slice(3, 4), /slice start anchor/);

      assert.throws(() => s.slice(3, 5), /slice start anchor/);

      assert.equal(s.slice(1, 5), 'bCDe');
    });

    it('does not error if slice is after removed characters', () => {
      const s = new MagicString('abcdef');

      s.remove(0, 2);

      assert.equal(s.slice(2, 4), 'cd');
    });
  });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@IWANABETHATGUY IWANABETHATGUY changed the title feat: nativeMagicString slice feat: nativeMagicString slice Jan 9, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 9, 2026

Benchmarks Rust

  • target: main(7d70b52)
  • pr: 01-08-feat_nativemagicstring_slice(1960898)
group                                                        pr                                     target
-----                                                        --                                     ------
bundle/bundle@multi-duplicated-top-level-symbol              1.00     68.6±2.28ms        ? ?/sec    1.05     72.4±1.80ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-sourcemap    1.00     74.7±2.09ms        ? ?/sec    1.01     75.6±2.50ms        ? ?/sec
bundle/bundle@rome_ts                                        1.01    110.3±2.41ms        ? ?/sec    1.00    109.3±2.38ms        ? ?/sec
bundle/bundle@rome_ts-sourcemap                              1.00    121.6±3.38ms        ? ?/sec    1.01    122.4±1.78ms        ? ?/sec
bundle/bundle@threejs                                        1.03     40.2±1.56ms        ? ?/sec    1.00     39.2±0.85ms        ? ?/sec
bundle/bundle@threejs-sourcemap                              1.02     45.7±0.96ms        ? ?/sec    1.00     44.7±0.75ms        ? ?/sec
bundle/bundle@threejs10x                                     1.01    396.8±5.02ms        ? ?/sec    1.00    394.4±7.93ms        ? ?/sec
bundle/bundle@threejs10x-sourcemap                           1.00    458.2±5.09ms        ? ?/sec    1.00    458.0±8.28ms        ? ?/sec
scan/scan@rome_ts                                            1.00     87.5±2.47ms        ? ?/sec    1.00     87.6±1.84ms        ? ?/sec
scan/scan@threejs                                            1.02     29.6±0.60ms        ? ?/sec    1.00     28.9±0.59ms        ? ?/sec
scan/scan@threejs10x                                         1.00    299.9±4.96ms        ? ?/sec    1.00    299.7±5.80ms        ? ?/sec

@IWANABETHATGUY IWANABETHATGUY force-pushed the 01-08-feat_nativemagicstring_slice branch 2 times, most recently from 0e806b2 to 80e474e Compare January 9, 2026 07:49
Copilot AI review requested due to automatic review settings January 9, 2026 07:49
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Rewrites the `slice` method in `string_wizard` to be a faithful 1:1 port
of the original JavaScript magic-string implementation (lines 598-650).

Key changes:
- Match the exact loop structure: find start chunk first, then iterate
- Intro handling: include if not start chunk OR chunk.start == start
- Content slicing: use same calculation as JS for sliceStart/sliceEnd
- Outro handling: include if !containsEnd OR chunk.end == end
- Error messages match JS format with period at end

Also enables two previously skipped tests in MagicString.test.ts.
@graphite-app
Copy link
Contributor

graphite-app bot commented Jan 9, 2026

Merge activity

@IWANABETHATGUY IWANABETHATGUY merged commit b20fec4 into main Jan 9, 2026
35 checks passed
@IWANABETHATGUY IWANABETHATGUY deleted the 01-08-feat_nativemagicstring_slice branch January 9, 2026 08:29
This was referenced Jan 14, 2026
shulaoda added a commit that referenced this pull request Jan 14, 2026
## [1.0.0-beta.60] - 2026-01-14

### 💥 BREAKING CHANGES

- tsconfig: enable auto-discovery by default (#7817) by @shulaoda

### 🚀 Features

- distinguish transformer diagnostics from parse errors (#7872) by @shulaoda
- emit transformer warnings instead of ignoring them (#7850) by @shulaoda
- node: add `output.codeSplitting` option and deprecate `output.advancedChunks` (#7855) by @hyf0
- nativeMagicString reset (#7828) by @IWANABETHATGUY
- nativeMagicString lastChar (#7819) by @IWANABETHATGUY
- dev/lazy: inject lazy compilation runtime automatically (#7816) by @hyf0
- nativeMagicString snip (#7818) by @IWANABETHATGUY
- nativeMagicString construct with options (#7814) by @IWANABETHATGUY
- nativeMagicString clone (#7813) by @IWANABETHATGUY
- nativeMagicString `insert` (#7812) by @IWANABETHATGUY
- nativeMagicString `slice` (#7807) by @IWANABETHATGUY
- nativeMagicString trim methods (#7800) by @IWANABETHATGUY
- make closeBundle hook receive the last error (#7278) by @Copilot

### 🐛 Bug Fixes

- when package only contains export default, cjsDefault didn't resolve correctly (#7873) by @IWANABETHATGUY
- inline __name calls for default exports  (#7862) by @IWANABETHATGUY
- improve variable renaming to avoid unnecessary shadowing in nested scopes (#7859) by @IWANABETHATGUY
- use correct index when inserting keepNames statements during export default transformation (#7853) by @IWANABETHATGUY
- transform non-static dynamic imports when `dynamicImportInCjs` is `false` (#7823) by @shulaoda
- dev/lazy: should include imported and non-executed modules in the patch (#7815) by @hyf0
- set ExportsKind to Esm when json is none object literal  (#7808) by @IWANABETHATGUY
- nativeMagicString move api (#7796) by @IWANABETHATGUY
- remove unnecessary exports after merging into commong and user defined entry (#7789) by @IWANABETHATGUY
- use output.name instead of chunk.name in mixed export warning (#7788) by @Copilot

### 🚜 Refactor

- generalize ParseError to OxcError with dynamic EventKind (#7868) by @shulaoda
- rust: rename `advanced_chunks` to `manual_code_splitting` (#7856) by @hyf0
- string_wizard error hanlding (#7830) by @IWANABETHATGUY
- remove `experimental.disableLiveBindings` option (#7820) by @sapphi-red
- node/test: run fixture tests in concurrent (#7790) by @hyf0
- move ConfigExport and RolldownOptionsFunction types to define-config (#7799) by @shulaoda
- cli: validate config after resolving and improve error message (#7798) by @shulaoda

### 📚 Documentation

- rebrand (#7670) by @yyx990803
- fix incorrect default value for propertyReadSideEffects (#7847) by @Copilot
- remove options pages and redirect to reference pages (#7834) by @sapphi-red
- options: inline types to option property pages (#7831) by @sapphi-red
- options: port checks.pluginTimings content from options page to reference page (#7832) by @sapphi-red
- options: use `@linkcode` where possible (#7824) by @sapphi-red
- options: port content from options page to reference page (#7822) by @sapphi-red
- options: add descriptions for output options (#7821) by @sapphi-red
- options: add description for input options (#7802) by @sapphi-red
- options: add description for `checks.*` (#7801) by @sapphi-red
- apis: add hook graph (#7671) by @sapphi-red

### 🧪 Testing

- add all valid combination of chunk exports related test (#7851) by @IWANABETHATGUY
- enable MagicString test after api return type alignment (#7797) by @IWANABETHATGUY
- init magic-string test (#7794) by @IWANABETHATGUY

### ⚙️ Miscellaneous Tasks

- vite-tests: configure git user for rebase operation (#7875) by @shulaoda
- rolldown_binding: remove v3 native plugins (#7837) by @shulaoda
- rolldown_binding: allow crate-type as lib (#7866) by @Brooooooklyn
- README.md: adjust position and size of rolldown logo (#7861) by @hyf0
- deps: update test262 submodule for tests (#7857) by @sapphi-red
- deps: update oxc to v0.108.0 (#7845) by @renovate[bot]
- deps: update dependency oxlint to v1.39.0 (#7849) by @renovate[bot]
- deps: update dependency oxfmt to ^0.24.0 (#7844) by @renovate[bot]
- deps: update npm packages (#7841) by @renovate[bot]
- deps: update rust crates (#7839) by @renovate[bot]
- deps: update github-actions (#7840) by @renovate[bot]
- use workspace edition for all crates (#7829) by @IWANABETHATGUY
- deps: update dependency oxlint-tsgolint to v0.11.0 (#7827) by @renovate[bot]
- deps: update napi to v3.8.2 (#7810) by @renovate[bot]
- remove outdated snapshot files (#7806) by @shulaoda
- deps: update crate-ci/typos action to v1.42.0 (#7792) by @renovate[bot]

Co-authored-by: shulaoda <[email protected]>
@sapphi-red sapphi-red mentioned this pull request Feb 14, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants