mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
v1.91.188.0 fix(pregate): resolve extensionless .jsx, .mts, .cts and index.jsx/tsx/mjs imports (#3340) (#3371)
* fix(pregate): resolve extensionless .jsx, .mts, .cts and index.jsx/tsx/mjs imports (#3340) RESOLVE_EXT had no .jsx and no index.jsx/index.tsx/index.mjs, so on a JSX repo every extensionless component import resolved nowhere and lanes failed with 'has no test lane'. Existing entries keep their order (it decides precedence). Fixes #3340 Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * v1.91.188.0 stamp Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
632bde77de
commit
1e3ee83c43
7 files changed
+70
-4
No files matched your search
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [1.91.188.0] - 2026-10-24
|
||||
|
||||
**Pregate maps extensionless .jsx imports to their tests (#3340).** On a Next.js or webpack JSX repo, `gstack-pregate` failed `lanes` with "has no test lane" for every component imported without an extension, so /ship's receipt refused and the only way out was shipping without one. The import resolver tried `.ts`, `.tsx`, `.js`, `.mjs` and `.cjs` but never `.jsx`, and a directory import found `index.ts` or `index.js` but not `index.jsx`, `index.tsx` or `index.mjs` (it fell back to selecting on the whole directory). Reported by adam-balazs.
|
||||
|
||||
### Fixed
|
||||
- **`lib/pregate/import-graph.ts`**: `RESOLVE_EXT` adds `.jsx`, `.mts`, `.cts`, `/index.tsx`, `/index.jsx` and `/index.mjs`. The existing entries keep their order, so a `.js` file still wins over a `.jsx` sibling. Relative, `paths` alias and `baseUrl` imports share the list, so all three resolve the new forms.
|
||||
|
||||
### Tests
|
||||
- `test/pregate-selection.test.ts` has three new cases: an extensionless import of a `.jsx`, `.mts` and `.cts` file; a directory import that reaches `index.jsx`, `index.tsx` and `index.mjs` and leaves the directory's other files unselected; and a jsconfig `paths` alias to a `.jsx` file, with a `.js` sibling that still wins. All three failed before the change.
|
||||
|
||||
### Docs
|
||||
- `docs/pregate.md` lists the extensions and index forms an extensionless import tries, in order.
|
||||
|
||||
## [1.91.187.0] - 2026-10-24
|
||||
|
||||
**Four docs now say what the code does: the unattended run contract, the gate reply grammar, the session-kind override and voice triggers.** All four were reported by readers who checked the docs against the code. `docs/unattended.md` promised that every workflow skill ends an unattended run with a run directory and a `GSTACK_RESULT` line, but only `/autoplan` has the steps and helpers that write one (#3281, thanks @exGeni). `gstack-gate --help` still taught the reply grammar from before 1.91.107.0, without `rec` or `as-is`, while the render it relays leads with `rec` (#3282, thanks @exGeni). `docs/OPENCLAW.md` said only `spawned` is honored as a `GSTACK_SESSION_KIND` override, but `unattended` is honored too (#3287, thanks @exGeni). The README said natural phrases activate the right skill, without saying that only `voice-triggers:` aliases reach a host and that the plain `triggers:` list does not (#2118, thanks @jinzheio).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.187.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.188.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
@@ -137,6 +137,10 @@ Two stages with separate clocks, both printed:
|
||||
included, are source code, so a `dependencies` `[]` declaration clears them and `empty_lanes`
|
||||
does not), transitively through
|
||||
non-test files; deleted and renamed files select the tests that named them.
|
||||
An extensionless JS/TS import (relative, a tsconfig/jsconfig `paths` alias or
|
||||
`baseUrl`) tries the exact path, then `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`,
|
||||
`.cjs`, `.mts`, `.cts`, then `index.ts`, `index.tsx`, `index.js`, `index.jsx`
|
||||
and `index.mjs` inside the directory; the first file that exists wins.
|
||||
A touched source file with no lane is `FAIL lanes — <file> has no test lane`,
|
||||
never a pass. Markdown that no test depends on is a docs/no-tests empty lane
|
||||
instead (`docs/no-tests: N documentation file(s)`), including when the base
|
||||
|
||||
@@ -47,7 +47,7 @@ const scanner = new Bun.Transpiler({ loader: 'tsx' });
|
||||
// that matches more text only gains an edge when it resolves to a repo file.
|
||||
const QUOTED = /['"`]([A-Za-z0-9_@][A-Za-z0-9_./@()[\]+-]*)['"`]/g;
|
||||
const JOIN = /\bjoin\(([^()]*)\)/g;
|
||||
const RESOLVE_EXT = ['', '.ts', '.tsx', '.js', '.mjs', '.cjs', '/index.ts', '/index.js'];
|
||||
const RESOLVE_EXT = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts', '/index.ts', '/index.tsx', '/index.js', '/index.jsx', '/index.mjs'];
|
||||
// A statement starts a line or follows `;` or a compound statement's `:`. The module spec is
|
||||
// non-empty and starts with a dot or a word character, so no two repetitions can share a dot or a space.
|
||||
const PY_IMPORT = /(?:^|[;:])[ \t\f]*(?:from[ \t]+(\.+(?:\w[\w.]*)?|\w[\w.]*)[ \t]+import[ \t]+(\([^)]*\)|[^\n;]+)|import[ \t]+([^\n;]+))/gm;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.187",
|
||||
"version": "1.91.188",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -82,6 +82,55 @@ describe('import graph (static imports ∪ repo-path literals ∪ bin convention
|
||||
expect(testsFor(g, 'notes/scratch.md')).toMatchObject({ tests: [], declaredNone: false });
|
||||
});
|
||||
|
||||
test('#3340: an extensionless import of a .jsx, .mts or .cts file maps to that file', () => {
|
||||
const repo = fixture();
|
||||
commit(repo, {
|
||||
'src/components/Foo.jsx': 'export default function Foo() { return null; }\n',
|
||||
'src/lib/m.mts': 'export const m = 1;\n',
|
||||
'src/lib/c.cts': 'export const c = 1;\n',
|
||||
'test/foo.test.jsx': "import Foo from '../src/components/Foo';\nimport { m } from '../src/lib/m';\nimport { c } from '../src/lib/c';\n",
|
||||
});
|
||||
const g = buildGraph(repo.work);
|
||||
for (const f of ['src/components/Foo.jsx', 'src/lib/m.mts', 'src/lib/c.cts']) {
|
||||
expect(testsFor(g, f).tests).toEqual(['test/foo.test.jsx']);
|
||||
expect(testsFor(g, f).provenance[0]).toContain('(import)');
|
||||
}
|
||||
});
|
||||
|
||||
test('#3340: a directory import resolves to its index.jsx, index.tsx or index.mjs, not only the dir/** fallback', () => {
|
||||
const repo = fixture();
|
||||
commit(repo, {
|
||||
'src/Bar/index.jsx': 'export default 1;\n',
|
||||
'src/Bar/other.js': 'export default 2;\n',
|
||||
'src/Baz/index.tsx': 'export default 1;\n',
|
||||
'src/Baz/other.js': 'export default 2;\n',
|
||||
'src/Qux/index.mjs': 'export default 1;\n',
|
||||
'src/Qux/other.js': 'export default 2;\n',
|
||||
'test/dirs.test.js': "import Bar from '../src/Bar';\nimport Baz from '../src/Baz';\nimport Qux from '../src/Qux';\n",
|
||||
});
|
||||
const g = buildGraph(repo.work);
|
||||
for (const f of ['src/Bar/index.jsx', 'src/Baz/index.tsx', 'src/Qux/index.mjs']) {
|
||||
expect(testsFor(g, f).tests).toEqual(['test/dirs.test.js']);
|
||||
expect(testsFor(g, f).provenance[0]).not.toContain('/**');
|
||||
}
|
||||
for (const f of ['src/Bar/other.js', 'src/Baz/other.js', 'src/Qux/other.js']) expect(testsFor(g, f).tests).toEqual([]);
|
||||
});
|
||||
|
||||
test('#3340: an import through a jsconfig paths alias reaches a .jsx file; an existing .js sibling still wins', () => {
|
||||
const repo = fixture();
|
||||
commit(repo, {
|
||||
'jsconfig.json': '{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }\n',
|
||||
'src/ui/Button.jsx': 'export default function Button() { return null; }\n',
|
||||
'src/ui/Card.js': 'export default 1;\n',
|
||||
'src/ui/Card.jsx': 'export default 2;\n',
|
||||
'test/ui.test.jsx': "import Button from '@/ui/Button';\nimport Card from '@/ui/Card';\n",
|
||||
});
|
||||
const g = buildGraph(repo.work);
|
||||
expect(testsFor(g, 'src/ui/Button.jsx').tests).toEqual(['test/ui.test.jsx']);
|
||||
expect(testsFor(g, 'src/ui/Card.js').tests).toEqual(['test/ui.test.jsx']);
|
||||
expect(testsFor(g, 'src/ui/Card.jsx').tests).toEqual([]);
|
||||
});
|
||||
|
||||
test('python: absolute, from-import submodule, relative and parenthesised imports and mock.patch targets map; stdlib, comments and docstrings do not', () => {
|
||||
const repo = fixture();
|
||||
commit(repo, {
|
||||
|
||||
Reference in new issue
Block a user