-
Notifications
You must be signed in to change notification settings - Fork 785
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
feat(compiler): consumer sourcemap support #3005
Conversation
@@ -103,6 +103,8 @@ export const getModuleLegacy = (_config: d.Config, compilerCtx: d.CompilerCtx, s | |||
potentialCmpRefs: [], | |||
staticSourceFile: null, | |||
staticSourceFileText: '', | |||
sourceMapPath: null, |
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.
Ryan: Consider why we don't put this in the build context, as that gets refreshed on every new build
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.
Maybe it's because these things are static?
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.
This may be a signal that a rename of these fields may be necessary
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.
AFAIK, this method helps build the moduleMap, I don't understand yet why it's named with "legacy" yet, but generally this should help to build the object that will ultimately be a part of the moduleMap.
Interestingly enough, this is in some way already on the buildCtx too, since buildCtx takes in the compilerCtx and uses that to do some internal stuff throughout the life of a build. So, this is technically available on the buildCtx, and may in fact be used by it too, but I don't yet know if it is for sure. At a cursory search through the codebase, it doesn't seem to be used. (I searched with .compilerCtx.moduleMap)
I assume it's set up this way to save on memory. If the module map was instantiated and populated for each build, there would be a lot of processing power and disk reads that would be prohibitive. So having it at a higher level cache and providing it down to the buildCtx should help mitigate that.
I swear I'll get around to an ADR on compilerCtx and buildCtx lol. There's a lot to unravel about these two concepts.
@@ -11,6 +11,8 @@ import { | |||
STENCIL_INTERNAL_HYDRATE_ID, | |||
} from './entry-alias-ids'; | |||
import ts from 'typescript'; | |||
import { basename } from 'path'; |
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.
Ryan: Should this be moved to the Node-Sys? We need to see if this is called in a Node context at all times or not?
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 think we should use the abstraction here.
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 took a look into this this AM, turns out that we do our own little polyfill of path
- there isn't a node-sys
version of path
. This is fine as-is
} | ||
|
||
const module = compilerCtx.moduleMap.get(config.globalScript); | ||
if (!module.sourceMapFileText) return { code: module.staticSourceFileText, map: null }; |
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.
Should we validate that module
exists here?
if (!module.sourceMapFileText) return { code: module.staticSourceFileText, map: null }; | |
if (!module) { | |
return null; | |
} | |
if (!module.sourceMapFileText) return { code: module.staticSourceFileText, map: null }; |
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 think this is a good idea imho.
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.
Should we resolve this?
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.
Not yet, let me put up a PR with a few tweaks to this file
|
||
const sourceMap: d.SourceMap = JSON.parse(module.sourceMapFileText); | ||
sourceMap.sources = sourceMap.sources.map((src) => basename(src)); | ||
return { code: module.staticSourceFileText, map: sourceMap }; |
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.
TODO I want to investigate this further with respect to these fields (they aren't the ones that were added in the PR)
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 think this is the signature of the LoadResult as a SourceDescription
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.
Similarly for a rollup plugin's transform method - TransformResult as a partial Source Description
@@ -52,7 +54,7 @@ export const appDataPlugin = ( | |||
return null; | |||
}, | |||
|
|||
load(id) { | |||
load(id): d.RollupLoadHook { |
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 been thinking a lot on this file.
I want to propose a couple refactoring items here that are not blocking, aren't worth adding to this PR, but worth talking about and adding a ticket to Jira if agreed upon.
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.
First, I think we should try to set a policy around our internal rollup plugins and their load
/transform
to always return either null, or the SourceDescription object. That way, extending this code to allow source maps to other new files later down the line aren't a problem, but also if we ever want to pass along an AST without a source map, we can do just that.
I don't necessarily agree about sometimes returning a string, or sometimes returning an object, and would love to see that as a policy for these two methods.
I rewrote this function to be:
load(id) {
const s = new MagicString(``);
let result = null;
if (id === STENCIL_APP_GLOBALS_ID) {
appendGlobalScripts(globalScripts, s);
result = {
code: s.toString(),
map: undefined,
};
} else if (id === STENCIL_APP_DATA_ID) {
appendNamespace(config, s);
appendBuildConditionals(config, build, s);
appendEnv(config, s);
result = {
code: s.toString(),
map: undefined,
};
} else if (id === config.globalScript) {
const module = compilerCtx.moduleMap.get(config.globalScript);
if (!module.sourceMapFileText) return { code: module.staticSourceFileText, map: null };
const sourceMap: d.SourceMap = JSON.parse(module.sourceMapFileText);
sourceMap.sources = sourceMap.sources.map((src) => basename(src));
result = {
code: module.staticSourceFileText,
map: sourceMap
};
}
return result;
},
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.
Second, and more contentious because I don't have a deeper understanding of the flow of data - I think in an effort to remove the if statements from the plugin, we should abstract this into three separate plugins.
- the STENCIL_APP_GLOBALS_ID plugin
- the STENCIL_APP_DATA_ID plugin
- the config.globalScript plugin
That way this file can be kept small and testable.
Meaning, we'd have three plugins with these load functions:
app-data-globals-plugin.ts
load(id) {
const s = new MagicString(``);
let result = null;
if (id === STENCIL_APP_GLOBALS_ID) {
appendGlobalScripts(globalScripts, s);
result = {
code: s.toString(),
map: undefined,
};
}
return result;
},
app-data-plugin.ts
load(id) {
const s = new MagicString(``);
let result = null;
if (id === STENCIL_APP_DATA_ID) {
appendNamespace(config, s);
appendBuildConditionals(config, build, s);
appendEnv(config, s);
result = {
code: s.toString(),
map: undefined,
};
}
return result;
},
app-data-globalScript-plugin.ts
load(id) {
let result = null;
if (id === config.globalScript) {
const module = compilerCtx.moduleMap.get(config.globalScript);
if (!module.sourceMapFileText) return { code: module.staticSourceFileText, map: null };
const sourceMap: d.SourceMap = JSON.parse(module.sourceMapFileText);
sourceMap.sources = sourceMap.sources.map((src) => basename(src));
result = {
code: module.staticSourceFileText,
map: sourceMap
};
}
return result;
},
I recognize this is a subset of the content of the original file, so this is naive. But this plugin is overloaded without a doubt.
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.
For future reference - I think passing around an AST instead of a string may improve the performance of our consumers compilation. See rollup/rollup#782
@@ -99,6 +99,7 @@ | |||
"jest-environment-node": "^26.6.2", | |||
"listr": "^0.14.3", | |||
"magic-string": "^0.25.7", | |||
"merge-source-map": "^1.1.0", |
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 have a question out to see if this needs to be added to
Line 9 in 925d4e9
const entryDeps = [ |
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.
NVM - got confirmation we do (need to add it) - #3016
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.
src/utils/sourcemaps.ts
Outdated
|
||
export const rollupSrcMapToObj = (rollupSrcMap: RollupSourceMap): d.SourceMap => { | ||
if (!rollupSrcMap) return null; | ||
if (typeof rollupSrcMap.toUrl === 'function') return JSON.parse(rollupSrcMap.toString()); |
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.
Question: If rollupSrcMap
is of type RollupSourceMap
, which is defined as:
export interface SourceMap {
...
toUrl(): string;
}
when is this case false
?
src/utils/sourcemaps.ts
Outdated
export const rollupSrcMapToObj = (rollupSrcMap: RollupSourceMap): d.SourceMap => { | ||
if (!rollupSrcMap) return null; | ||
if (typeof rollupSrcMap.toUrl === 'function') return JSON.parse(rollupSrcMap.toString()); | ||
if (typeof rollupSrcMap === 'string') return JSON.parse(rollupSrcMap); |
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.
Question: If rollupSrcMap
is of type RollupSourceMap
, which is defined as:
export interface SourceMap {
...
}
when is this case true
?
src/utils/sourcemaps.ts
Outdated
if (!rollupSrcMap) return null; | ||
if (typeof rollupSrcMap.toUrl === 'function') return JSON.parse(rollupSrcMap.toString()); | ||
if (typeof rollupSrcMap === 'string') return JSON.parse(rollupSrcMap); | ||
return rollupSrcMap; |
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.
Note - I'm gonna dig more into this one. This feels like it could be brittle, as it assumes a level of parity between our Sourcemap representation and Rollup's. But I need to dig in a bit more
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 think your observations are quite right.
I have vague recollections of initially using this function to turn a string OR RollupSourceMap to a sourceMap proper.
But later changed; only using it to convert the rollup variant so these checks became obsolete.
I think I changed the name of the function but kept the checks in - either 'cos I forgot or thought it didn't do any harm
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.
That makes sense, thanks! I'm gonna dig into just to be sure 🙂
@@ -21,6 +21,7 @@ export const generateCjs = async ( | |||
entryFileNames: '[name].cjs.js', | |||
assetFileNames: '[name]-[hash][extname]', | |||
preferConst: true, | |||
sourcemap: !!config.sourceMap, |
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.
Note for future us: Someday I'd like to move us to a model where we bifurcate the idea of a Stencil config into two internally - one model for unvalidated user input, and other for validated configuration. We do this to some degree today (we surely validate the config) - but a separate interface would allow us to narrow the types and avoid things like this moving forward.
…t-custom-elements, dist, esm, esm-es5 and cjs. Added option to StencilConfig type options.
this PR replaces our custom typings for rollup hooks with those provided by rollup
add merge-source-map to license file list, as it will be included in the Stencil distributable
simplify the transformation of sourcemaps by removing code paths that I don't believe will be followed. add tests to the sourcemaps utility function
ensure that upon validating a user's stencil config, the value of the 'sourcemap' field is never undefined. remove unneeded 'bang bang' operators after forcing souremap to boolean
this commit refactors & renames `src/compiler/config/test/validate-ts-sourcemap.spec.ts` to `src/compiler/config/test/validate-config-sourcemap.spec.ts` the file was renamed to align with the unit test file `validate-config.spec.ts`, found in the same directory. This file was not merged with `validate-config.spec.ts` to avoid more refactoring than necessary. the refactoring of the test file consists of: - renaming the suite & associated tests for clarity - removing unnecessary filesystem calls - refactoring generating a `LoadConfigInit` entity out to a helper fn - variable renaming
this PR makes changes to the Stencil-defined type declaration file for `merge-source-map` `merge-source-map` does not offer any `d.ts` file, nor does it have a third party offering on Definitely Typed, so the intent of this effort was to see "how correct is our type definition for our use case, while not deviating from the actual library's implicit typings".
e8cab1d
to
7ff03dc
Compare
includeContent: true, | ||
hires: true, | ||
}); | ||
return { code: results.outputText, map: sourceMapMerge(codeMap, sourceMap) }; |
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.
@johnjenkins Do you recall why we want to merge the sourcemaps here?
I did some digging, at comparing the sourcemap generated by the TS compiler (left in the image below) and the one generated by MagicString (right in the image below):
It seems the only difference is the name of file
and mappings
.
I think per the spec for sourcemaps, file
should point to globals.js
in this situation (and should be easy enough to handle).
The hires
(high resolution) mappings are what I'm intrigued by. I took at look at the MagicString Docs
whether the mapping should be high-resolution. Hi-res mappings map every single character, meaning (for example) your devtools will always be able to pinpoint the exact location of function calls and so on. With lo-res mappings, devtools may only be able to identify the correct line - but they're quicker to generate and less bulky. If sourcemap
Was the idea here to get sourcemaps with higher fidelity, then merge them back into the ones generated by TS?
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 @rwaskiewicz - yes the difference in mappings is the important thing ...
We need to merge sourcemaps because we need to account for the extra import / export strings of code that can get added and to this end I needed to get creative!
- Turn the incoming TS string to a MagicString - this will track the changes we make to the original TS source string and can generate a sourcemap tracking these changes.
- Transpile the TS using the
transpileModule
- this generates another sourcemap however the typescript compiler has no idea of the earlier amends made to the source TS therefore the sourcemap generated does not reflect these amends and is incorrect. - Merge the original sourcemap from MagicString and the new one from typescript - all changes made to the original source now being accounted for.
This is a little convoluted I agree - I was just working with what was there originally and appending strings of code to source input should be avoided in general for this reason.
For future devs - try to make these amends to sourcecode within TS plugins / AST to avoid this hinkiness.
Regarding the hires
property - I simply applied it because the weird process described and corresponding merge would produce slightly incorrect sourcemaps and this fixed it.
Attached is 2 screen caps. 1 with hires
- each console points to the correct line from the original source. 2 without hires
source lines highlighted can be out by 1 (or 2)
https://user-images.githubusercontent.com/5030133/134250837-edc67824-f668-4b85-8b34-4966017cc70c.mov
https://user-images.githubusercontent.com/5030133/134250827-634f7e57-5e24-451c-a353-21dde518489b.mov
this commit adds additional typings to `generateRollupOutput` to help make it easier to understand the map() function call that is invoked on the results of calling `build.generate()` sourcemap is an optional property on rollup source maps, update our representation extract the labels used in timespans to variables, to make it less likely we'll mess up their content in two different callsites this commit also adds return types to the methods updated
duplicate the typings for rollup (again). Stencil cannot resolve the rollup typings in the private compiler api typings at run time for consuming projects. As a result, we duplicate the sourcemap typings from rollup partial revert of #3009
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.
Looking good! Going to try this out in a couple local libraries and circle back.
const { output } = await build.generate(options); | ||
return output.map((chunk) => { | ||
const { output }: { output: [OutputChunk, ...(OutputChunk | OutputAsset)[]] } = await build.generate(options); | ||
return output.map((chunk: OutputChunk | OutputAsset) => { |
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.
For my own understanding, what the difference between these two - Chunk and Asset?
} | ||
|
||
const module = compilerCtx.moduleMap.get(config.globalScript); | ||
if (!module.sourceMapFileText) return { code: module.staticSourceFileText, map: null }; |
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.
Should we resolve this?
@@ -346,6 +356,13 @@ export interface BundleEntryInputs { | |||
[entryKey: string]: string; | |||
} | |||
|
|||
/** | |||
* A note regarding Rollup types: |
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.
@rwaskiewicz do you consider this Technical debt? Would we have a ticket in the backlog already if so?
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 don't think so, it's an archtectural aspect of the system (I think, in so far as I can tell rn). I'll add something to the backlog as a low priority spike
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.
STENCIL-175
add a check to verify that a global script exists in the moduleMap on the compiler context before accessing fields on it to prevent compiler time errors. this is one of the items we discussed as a team in the GH comments of #3005, and is being implemented here reworked how sourcemaps are generated. this commit restores the original code generation where the platform, source code, and default export are concatenated, which allows us to reserve generating a magic string until/if we're using source maps remove includeContent field from the generation of the sourcemap, which does not appear to be needed, strictly speaking
this commit introduces a new type, OptimizeJsResult, to be used to name the shape of the object that is returned by various methods that are used to minify a user's JavaScript. using this type in the return signature of methods + in the declaration of objects allows us to eliminate unnessary type assertions on the individual fields of said objects. * add JSDoc to minification to the functions that were investigated as a part of this effort. this is done in an effort to make the dissemination of code within Stenicl slightly better. of course, we incur the cost of maintaining these comments. * account for minification with sourcemaps sourcemaps in some regard are at odds with attempting to minize and compress our JavaScript files. Ideally there's no fidelity lost when compressing JS, but in some cases, we get inconsistent behaviors across browsers when we have both 'sourcemap' and 'compress' enabled in our terser configuration. after diffing through the terser source/docs, I think our best route forward for now is to simply disable the compress step. I took a sample Stencil app and did a `diff` of the output where in one scenario we didn't set compress to undefined, and one where we did set it to undefined. other than the sourcemaps themselves, the differences were rather minimal, and I believe this shouldn't have a large impact on the output
add a check to verify that a global script exists in the moduleMap on the compiler context before accessing fields on it to prevent compiler time errors. this is one of the items we discussed as a team in the GH comments of #3005, and is being implemented here reworked how sourcemaps are generated. this commit restores the original code generation where the platform, source code, and default export are concatenated, which allows us to reserve generating a magic string until/if we're using source maps remove includeContent field from the generation of the sourcemap, which does not appear to be needed, strictly speaking
add a check to verify that a global script exists in the moduleMap on the compiler context before accessing fields on it to prevent compiler time errors. this is one of the items we discussed as a team in the GH comments of #3005, and is being implemented here reworked how sourcemaps are generated. this commit restores the original code generation where the platform, source code, and default export are concatenated, which allows us to reserve generating a magic string until/if we're using source maps
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.
LGTM
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 and works great! AWESOME work @johnjenkins and @rwaskiewicz!!
this commit adds sourcemap functionality for consumers of the following output targets: - dist-custom-elements - dist-custom-elements-bundle - www source maps are produced for JavaScript files that are emitted by running the Stencil compiler. this functionality is off by default, and must be enabled by setting the `sourceMap: true` flag in the root of a project's Stencil configuration file object
Great work everyone! 👏 💯 |
Woohoo can't wait to use this! Thanks for all effort! |
Beautiful work lads, just beautiful <3 |
This PR is an exact copy of #2892. After Prettier was added to the codebase, merge conflicts arose despite our best efforts. I took some time to clear them out on a separate branch. But a large majority of the work here is attributed to @johnjenkins, without whom we would not be nearly this far along.
This branch continues that work, as the Stencil team continues to refine sourcemap work and prepare it to be merged into the main branch.
Note: We should validate the issues that this closes by leveraging the list that John has created in their PR summary
fixes #1744
fixes #1650
fixes #219
fixes #1255