-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Detect/resolve ambiguous script names #9848
Conversation
Hi there, I'm looking for some early feedback on this PR as well as pointers to where any relevant tests may be located. I described my basic thought process here: #9249 (comment) Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for your PR! You're off to a great start 🙂
Looks like there are no existing tests for this code. Would be great if you could add some for sure!
Happy to. I'll need to modify the test project state to simulate different combinations of script files existing. Can you point me to where those types of preconditions might be established, or an example test that does something similar? |
I'd lean on mocking instead of using the test project, if possible. We've started using |
Examples of error messages. Note that previously the error message did not include the file extension in the available scripts.
And case when no script name is specified still works:
Examples of valid executions: |
Those are all awesome! |
Thanks! It's also worth pointing out that I think the only reason we can't just use the same console.error(
c.error(
`\n${
!targetMatch.length ? 'No script' : 'Multiple scripts'
} called ${c.underline(
targetScriptPath.ext
? targetScriptPath.base
: targetScriptPath.name + '.{js,jsx,ts,tsx}'
)} in ./scripts folder.\n`
)
) |
Thanks for the pointer. I was able to put together a test that works for a basic case. Mind taking a look and letting me know if I'm on the right track? Thanks! //
// - execHandler.test.js
//
import { vol } from 'memfs'
import { runScriptFunction as mockRunScriptFunction } from '../../lib/exec'
import { handler } from '../execHandler'
jest.mock('fs', () => require('memfs').fs)
jest.mock('../../lib/exec')
const redwoodProjectPath = '/redwood-app'
process.env.RWJS_CWD = redwoodProjectPath
describe('execHandler', () => {
afterEach(() => {
vol.reset()
mockRunScriptFunction.mockRestore()
})
describe('handler', () => {
it('verify exec script', async () => {
const mockExit = jest.spyOn(process, 'exit').mockImplementation()
const fooScript =
'export default async ({ args }) => { console.log(`:: Executing script ${__filename} ::`) }'
vol.fromNestedJSON(
{
'redwood.toml': '',
scripts: {
'foo.js': fooScript,
},
},
redwoodProjectPath
)
await handler({ name: 'foo', arg1: 'a1', arg2: 'a2' })
expect(mockExit).not.toHaveBeenCalled()
expect(mockRunScriptFunction).toHaveBeenCalledWith({
path: '/redwood-app/scripts/foo',
functionName: 'default',
args: { args: { arg1: 'a1', arg2: 'a2' } },
})
mockExit.mockRestore()
})
})
}) |
A few comments on the test:
|
Thanks for the detailed guidance! I'll address your points as I get together the rest of the test cases, hopefully wrapping up later today. Quick question regarding this one:
I was planning to write separate tests focused on testing the changes to |
it('resolves and runs script with no extension', async () => { | ||
const expectedScriptReturn = Math.floor(Math.random() * 1000) | ||
const fooScript = ` | ||
default async ({ args }) => ${expectedScriptReturn} | ||
module.exports = default | ||
` | ||
|
||
vol.fromNestedJSON( | ||
{ | ||
'redwood.toml': '', | ||
scripts: { | ||
'foo.js': fooScript, | ||
}, | ||
}, | ||
redwoodProjectPath | ||
) | ||
|
||
// NOTE: this test currently fails with: | ||
// "Error: Cannot find module '/redwood-app/scripts/foo.js' from 'src/lib/exec.js'" | ||
await expect( | ||
runScriptFunction({ | ||
path: '/redwood-app/scripts/foo', | ||
functionName: 'default', | ||
}) | ||
).resolves.toBe(expectedScriptReturn) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure how to test running the actual script, at least not when using memfs.
The code in question calls require('/redwood-app/scripts/foo.js')
, but this fails to load the test script module, probably because the test process is running within the context of the actual framework directory (e.g. '/Users/user/dev/redwoodjs/redwood'), not from the imaginary 'memfs' file system.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @Tobbe, in this case should I look to modify/add an integration test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found https://github.com/streamich/fs-monkey/blob/master/docs/api/patchRequire.md to help with the require
problem 🙂 Take a look in the tests I pushed
Sorry I went radio silent on this one @codersmith 📻 I'll try to give it some thought before the end of the week |
No problem, understand. Thanks for the vitest updates |
@codersmith I added a few tests. I'm not super happy with them. But mostly because the way Also left a TODO comment in the test that I'd like you to take a look at, and see if you can come up with something better. |
Just to be clear, are you referring to being able to write the test script using module vs common syntax like: export default async ({ args }) => ${expectedScriptReturn} (fails with "Module /redwood-app/scripts/fooModule.js:2 seems to be an ES Module but shipped in a CommonJS package.") vs module.exports = {
default: ({ args }) => ${expectedScriptReturn}
} (succeeds, but isn't typical syntax for RW scripts.) One thing that came to mind was to switch the script runner from using // old
const script = require(resolveSourcePath(scriptPath))
const returnValue = await script[functionName](args)
-------
// new
const returnValue = await import(resolveSourcePath(scriptPath)).then(
({ default: script }) => script[functionName](args)
) But that gets pretty tricky, as now we can't use |
I got that message too 😄 What happens when you start testing with ts scripts? /scripts/fooTS.ts I don't even know if patchRequire supports that. So you might have to get creative! |
vi.mock('@redwoodjs/project-config', () => { | ||
const path = require('path') | ||
const BASE_PATH = '/redwood-app' | ||
return { | ||
resolveFile: (path) => path, | ||
getPaths: () => { | ||
return { | ||
base: BASE_PATH, | ||
scripts: path.join(BASE_PATH, 'scripts'), | ||
} | ||
}, | ||
} | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Tobbe, I'm just getting back to this PR, and noticed that after the tests were switched from jest to vitest that they're not working.
I could use some help figuring out the issue, which I think has to do with setting up mocks for project-config
based on what I'm seeing other tests do, but I haven't been able to get it working.
For reference, I temporarily created two different implementations of the same test case:
execHandler-vitest.test.js
execHandler-jest.test.js
The jest test works, but I'm getting an error with vitest: "Could not find a "redwood.toml" file, are you sure you're in a Redwood project?"
Is this something you've seen before, or do you have an ideas on what changes I should make to get the vitest version running?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tried a few different variations of declaring this mock, such these, but I get the same error.
vi.mock('@redwoodjs/project-config', async (importOriginal) => {
const path = require('path')
const originalProjectConfig = await importOriginal()
const BASE_PATH = '/redwood-app'
return {
...originalProjectConfig,
getPaths: () => {
return {
base: BASE_PATH,
scripts: path.join(BASE_PATH, 'scripts'),
}
},
}
})
and
vi.mock('@redwoodjs/project-config', () => {
const BASE_PATH = '/redwood-app'
return {
resolveFile: (path) => path,
getPaths: () => {
return {
base: BASE_PATH,
scripts: path.join(BASE_PATH, 'scripts'),
}
},
getConfigPath: () => {
return 'redwood.toml'
},
getConfig: () => {
return {}
},
}
})
0b9dc57
to
68ef4eb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great. Tested locally too and it works nicely.
…pload-link * 'main' of github.com:redwoodjs/redwood: (52 commits) linting: enable 'typescript-eslint/await-thenable' rule (redwoodjs#11311) refactor(api): Add conditional exports to package.json (redwoodjs#11307) chore(readme): Add my middle initials (redwoodjs#11310) chore(README): Fix formatting. One row (redwoodjs#11309) chore(README): Move Kris from Maintainer to Alumni (redwoodjs#11308) fix(codemods): Move away from babel for building package (redwoodjs#11306) fix(api): Move away from babel for building package (redwoodjs#11305) fix(internal): Move away from babel for building package (redwoodjs#11304) fix(auth-providers): Move away from babel for building 'api' packages (redwoodjs#11301) fix(auth-providers): Move away from babel for building 'setup' packages (redwoodjs#11303) chore(deps): Remove webpack related dependencies (redwoodjs#11299) chore(build): build core with esbuild (redwoodjs#11298) chore(exec test): Clean up (redwoodjs#11302) Detect/resolve ambiguous script names (redwoodjs#9848) chore(lint): tidy up the prettier ignore (redwoodjs#11297) feat(context): Build and publish package as dual esm/cjs (redwoodjs#11294) chore(lint): fix prettier configs and ignores (redwoodjs#11295) chore(docs): update prettier config and format docs content (redwoodjs#11293) chore(check): Refactor 'yarn check' away from being a standalone node script (redwoodjs#11292) chore: delete crowdin config file (redwoodjs#11291) ...
…ads-extension * 'main' of github.com:redwoodjs/redwood: (52 commits) linting: enable 'typescript-eslint/await-thenable' rule (redwoodjs#11311) refactor(api): Add conditional exports to package.json (redwoodjs#11307) chore(readme): Add my middle initials (redwoodjs#11310) chore(README): Fix formatting. One row (redwoodjs#11309) chore(README): Move Kris from Maintainer to Alumni (redwoodjs#11308) fix(codemods): Move away from babel for building package (redwoodjs#11306) fix(api): Move away from babel for building package (redwoodjs#11305) fix(internal): Move away from babel for building package (redwoodjs#11304) fix(auth-providers): Move away from babel for building 'api' packages (redwoodjs#11301) fix(auth-providers): Move away from babel for building 'setup' packages (redwoodjs#11303) chore(deps): Remove webpack related dependencies (redwoodjs#11299) chore(build): build core with esbuild (redwoodjs#11298) chore(exec test): Clean up (redwoodjs#11302) Detect/resolve ambiguous script names (redwoodjs#9848) chore(lint): tidy up the prettier ignore (redwoodjs#11297) feat(context): Build and publish package as dual esm/cjs (redwoodjs#11294) chore(lint): fix prettier configs and ignores (redwoodjs#11295) chore(docs): update prettier config and format docs content (redwoodjs#11293) chore(check): Refactor 'yarn check' away from being a standalone node script (redwoodjs#11292) chore: delete crowdin config file (redwoodjs#11291) ...
Detects and resolves ambiguous script name combinations like
[foo.js, foo.ts]
or[foo.ts, foo.json]
when runningyarn rw exec foo
.Fixes #9249
Problem
#9249 identified that when you have a script file called
scripts/foo.ts
along with a JSON data file with the same namescripts/foo.json
, that trying to runyarn rw exec foo
breaks.The problem is that when calling
require('foo')
, require finds the JSON file first and loads that vs. loading the.ts
file.Solution
There are two parts to the fix:
Resolve any ambiguity ahead of trying to load and run the script.
For example, if the script name is fully qualified e.g.
yarn rw exec foo.ts
, then that should work.But if two scripts exist such as
[foo.js, foo.ts]
, and the command isyarn rw exec foo
, then this is ambiguous and a nice error message should be displayed to help the user clarify their intent.Any non-script files (like JSON files, etc.) should not conflict when resolving a potential script file. Script files are identified with extensions
.{js,jsx,ts,tsx}
To do this, we can resolve
foo
intofoo.ts
prior to attempt loading it withrequire
.EDIT (by @Tobbe):
Example output:
Normally it'll just list the names of the scripts. But if there is a conflict between names (two or more files with the same name, only different file extensions) then the file extension is included in the output
.json
files (or other files that we can't execute) are not included in the list of scriptsRunning a script by just the name prints an error if there's ambiguity
And as for testing, unfortunately we can't unit test actually running the script. The issue is with CJS/ESM (require vs import) and vitest. So for now only the listing of scripts is tested. Running the scripts has only been tested manually.